Source code for fastplotlib.graphics.features._mesh

from typing import Any, Sequence

import numpy as np
import pygfx

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

from ._positions import VertexPositions
from ...utils.functions import get_cmap
from ...utils.triangulation import triangulate


def resolve_cmap_mesh(cmap) -> pygfx.TextureMap | None:
    """Turn a user-provided in a pygfx.TextureMap, supporting 1D, 2D and 3D data."""

    if cmap is None:
        pygfx_cmap = None
    elif isinstance(cmap, pygfx.TextureMap):
        pygfx_cmap = cmap
    elif isinstance(cmap, pygfx.Texture):
        pygfx_cmap = pygfx.TextureMap(cmap)
    elif isinstance(cmap, (str, dict)):
        pygfx_cmap = pygfx.cm.create_colormap(get_cmap(cmap))
    else:
        map = np.asarray(cmap)
        if map.ndim == 2:  # 1D plus color
            pygfx_cmap = pygfx.cm.create_colormap(cmap)
        else:
            tex = pygfx.Texture(map, dim=map.ndim - 1)
            pygfx_cmap = pygfx.TextureMap(tex)

    return pygfx_cmap


[docs] class MeshIndices(VertexPositions): event_info_spec = [ { "dict key": "key", "type": "slice, index (int) or numpy-like fancy index", "description": "key at which vertex indices were indexed/sliced", }, { "dict key": "value", "type": "int | float | array-like", "description": "new data values for indices that were changed", }, ] def __init__( self, data: Any, isolated_buffer: bool = True, property_name: str = "indices" ): """ Manages the vertex indices buffer shown in the graphic. Supports fancy indexing if the data array also supports it. """ data = self._fix_data(data) super().__init__( data, isolated_buffer=isolated_buffer, property_name=property_name ) def _fix_data(self, data): if data.ndim != 2 or data.shape[1] not in (3, 4): raise ValueError( f"indices must be of shape: [n_vertices, 3] or [n_vertices, 4], " f"you passed an array of shape: {data.shape}" ) return data.astype("i4")
[docs] class MeshCmap(GraphicFeature): event_info_spec = [ { "dict key": "value", "type": "str | dict | pygfx.TextureMap | pygfx.Texture | np.ndarray", "description": "new cmap", }, ] def __init__( self, value: str | dict | pygfx.TextureMap | pygfx.Texture | np.ndarray | None, property_name: str = "cmap", ): """Manages a mesh colormap""" self._value = value super().__init__(property_name=property_name) @property def value( self, ) -> str | dict | pygfx.TextureMap | pygfx.Texture | np.ndarray | None: return self._value @block_reentrance def set_value( self, graphic, value: str | dict | pygfx.TextureMap | pygfx.Texture | np.ndarray | None, ): graphic.world_object.material.map = resolve_cmap_mesh(value) self._value = value event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event)
def surface_data_to_mesh(data: np.ndarray) -> tuple[np.ndarray, np.ndarray]: """ surface data to mesh positions and indices expects data that is of shape: [m, n, 3] or [m, n] """ data = np.asarray(data) if data.ndim == 2: # "image" of z values passed # [m, n] -> [n_vertices, 3] y = ( np.arange(data.shape[0]) .reshape(data.shape[0], 1) .repeat(data.shape[1], axis=1) ) x = ( np.arange(data.shape[1]) .reshape(1, data.shape[1]) .repeat(data.shape[0], axis=0) ) positions = np.column_stack((x.ravel(), y.ravel(), data.ravel())) else: if data.ndim != 3: raise ValueError( f"expect data that is of shape: [m, n, 3], [m, n]\n" f"you passed: {data.shape}" ) if data.shape[2] != 3: raise ValueError( f"expect data that is of shape: [m, n, 3], [m, n]\n" f"you passed: {data.shape}" ) # [m, n, 3] -> [n_vertices, 3] positions = data.reshape(-1, 3) # Create faces w = data.shape[1] i = np.arange(data.shape[0] - 1) j = np.arange(w - 1) j, i = np.meshgrid(j, i, indexing="ij") start = j.ravel() + w * i.ravel() indices = np.column_stack([start, start + 1, start + w + 1, start + w]) return positions, indices
[docs] class SurfaceData(GraphicFeature): event_info_spec = [ { "dict key": "value", "type": "np.ndarray", "description": "new surface data", }, ] def __init__(self, value: np.ndarray | Sequence, property_name: str = "data"): self._value = np.asarray(value, dtype=np.float32) super().__init__(property_name=property_name) @property def value(self) -> np.ndarray: return self._value @block_reentrance def set_value(self, graphic, value: np.ndarray): positions, indices = surface_data_to_mesh(value) graphic.positions = positions graphic.indices = indices # if cmap is a 1D texture we need to set the texcoords again using new z values if graphic.world_object.material.map is not None: if graphic.world_object.material.map.texture.dim == 1: mapcoords = positions[:, 2] if graphic.clim is None: clim = mapcoords.min(), mapcoords.max() else: clim = graphic.clim mapcoords = (mapcoords - clim[0]) / (clim[1] - clim[0]) graphic.mapcoords = mapcoords self._value = value event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event)
def triangulate_polygon(data: np.ndarray | Sequence): """vertices of shape [n_vertices , 2] -> positions, indices""" data = np.asarray(data, dtype=np.float32) err_msg = ( f"polygon vertex data must be of shape [n_vertices, 2], you passed: {data}" ) if data.ndim != 2: raise ValueError(err_msg) if data.shape[1] != 2: raise ValueError(err_msg) if len(data) >= 3: indices = triangulate(data) else: indices = np.arange((0, 3), np.int32) data = np.column_stack([data, np.zeros(data.shape[0], dtype=np.float32)]) return data, indices class PolygonData(GraphicFeature): event_info_spec = [ { "dict key": "value", "type": "np.ndarray", "description": "new polygon vertex data", }, ] def __init__(self, value: np.ndarray, property_name: str = "data"): self._value = np.asarray(value, dtype=np.float32) super().__init__(property_name=property_name) @property def value(self) -> np.ndarray: return self._value @block_reentrance def set_value(self, graphic, value: np.ndarray | Sequence): value = np.asarray(value, dtype=np.float32) positions, indices = triangulate_polygon(value) geometry = graphic.world_object.geometry # Need larger (or smaller) buffer? Scale up/down with factors of 2. need_position_size = 2 ** int(np.ceil(np.log2(max(8, len(positions))))) if need_position_size != geometry.positions.nitems: arr = np.zeros((need_position_size, 3), np.float32) geometry.positions = pygfx.Buffer(arr) need_indices_size = 2 ** int(np.ceil(np.log2(max(8, len(indices))))) if need_indices_size != geometry.indices.nitems: arr = np.zeros((need_indices_size, 3), np.int32) geometry.indices = pygfx.Buffer(arr) geometry.positions.data[: len(positions)] = positions geometry.positions.data[len(positions) :] = ( positions[-1] if len(positions) else (0, 0, 0) ) geometry.positions.draw_range = 0, len(positions) geometry.positions.update_full() geometry.indices.data[: len(indices)] = indices geometry.indices.data[len(indices) :] = 0 geometry.indices.draw_range = 0, len(indices) geometry.indices.update_full() # send event if len(self._event_handlers) < 1: return event = GraphicFeatureEvent(self._property_name, {"value": self.value}) # calls any events self._call_event_handlers(event)