diff --git a/examples/gridplot/multigraphic_gridplot.py b/examples/gridplot/multigraphic_gridplot.py index cbf546e2a..2ccb3a6e0 100644 --- a/examples/gridplot/multigraphic_gridplot.py +++ b/examples/gridplot/multigraphic_gridplot.py @@ -106,7 +106,7 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: gaussian_cloud2 = np.random.multivariate_normal(mean, covariance, n_points) # add the scatter graphics to the figure -figure["scatter"].add_scatter(data=gaussian_cloud, sizes=2, cmap="jet") +figure["scatter"].add_scatter(data=gaussian_cloud, sizes=2, cmap="jet", uniform_color=False) figure["scatter"].add_scatter(data=gaussian_cloud2, colors="r", sizes=2) figure.show() diff --git a/examples/line/line.py b/examples/line/line.py index f7839a1c4..e388adb21 100644 --- a/examples/line/line.py +++ b/examples/line/line.py @@ -30,11 +30,11 @@ sine = figure[0, 0].add_line(data=sine_data, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine = figure[0, 0].add_line(data=cosine_data, thickness=12, cmap="autumn") +cosine = figure[0, 0].add_line(data=cosine_data, thickness=12, cmap="autumn", uniform_color=False) # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc = figure[0, 0].add_line(data=sinc_data, thickness=5, colors=colors) +sinc = figure[0, 0].add_line(data=sinc_data, thickness=5, colors=colors, uniform_color=False) figure[0, 0].axes.grids.xy.visible = True figure.show() diff --git a/examples/line/line_cmap.py b/examples/line/line_cmap.py index 3d2b5e8c9..9e07a5aeb 100644 --- a/examples/line/line_cmap.py +++ b/examples/line/line_cmap.py @@ -27,7 +27,8 @@ data=sine_data, thickness=10, cmap="plasma", - cmap_transform=sine_data[:, 1] + cmap_transform=sine_data[:, 1], + uniform_color=False, ) # qualitative colormaps, useful for cluster labels or other types of categorical labels @@ -36,7 +37,8 @@ data=cosine_data, thickness=10, cmap="tab10", - cmap_transform=labels + cmap_transform=labels, + uniform_color=False, ) figure.show() diff --git a/examples/line/line_cmap_more.py b/examples/line/line_cmap_more.py index c7c0d80f4..cb3b22808 100644 --- a/examples/line/line_cmap_more.py +++ b/examples/line/line_cmap_more.py @@ -26,21 +26,43 @@ line0 = figure[0, 0].add_line(sine, thickness=10) # set colormap along line datapoints, use an offset to place it above the previous line -line1 = figure[0, 0].add_line(sine, thickness=10, cmap="jet", offset=(0, 2, 0)) +line1 = figure[0, 0].add_line(sine, thickness=10, cmap="jet", uniform_color=False, offset=(0, 2, 0)) # set colormap by mapping data using a transform # here we map the color using the y-values of the sine data # i.e., the color is a function of sine(x) -line2 = figure[0, 0].add_line(sine, thickness=10, cmap="jet", cmap_transform=sine[:, 1], offset=(0, 4, 0)) +line2 = figure[0, 0].add_line( + sine, + thickness=10, + cmap="jet", + cmap_transform=sine[:, 1], + uniform_color=False, + offset=(0, 4, 0), +) # make a line and change the cmap afterward, here we are using the cosine instead fot the transform -line3 = figure[0, 0].add_line(sine, thickness=10, cmap="jet", cmap_transform=cosine[:, 1], offset=(0, 6, 0)) +line3 = figure[0, 0].add_line( + sine, + thickness=10, + cmap="jet", + cmap_transform=cosine[:, 1], + uniform_color=False, + offset=(0, 6, 0) +) + # change the cmap line3.cmap = "bwr" # use quantitative colormaps with categorical cmap_transforms labels = [0] * 25 + [1] * 5 + [2] * 50 + [3] * 20 -line4 = figure[0, 0].add_line(sine, thickness=10, cmap="tab10", cmap_transform=labels, offset=(0, 8, 0)) +line4 = figure[0, 0].add_line( + sine, + thickness=10, + cmap="tab10", + cmap_transform=labels, + uniform_color=False, + offset=(0, 8, 0), +) # some text labels for i in range(5): diff --git a/examples/line/line_colorslice.py b/examples/line/line_colorslice.py index b6865eadb..2a74c73cf 100644 --- a/examples/line/line_colorslice.py +++ b/examples/line/line_colorslice.py @@ -30,7 +30,8 @@ sine = figure[0, 0].add_line( data=sine_data, thickness=5, - colors="magenta" + colors="magenta", + uniform_color=False, # initialize with same color across vertices, but we will change the per-vertex colors later ) # you can also use colormaps for lines! @@ -38,6 +39,7 @@ data=cosine_data, thickness=12, cmap="autumn", + uniform_color=False, offset=(0, 3, 0) # places the graphic at a y-axis offset of 3, offsets don't affect data ) @@ -47,6 +49,7 @@ data=sinc_data, thickness=5, colors=colors, + uniform_color=False, offset=(0, 6, 0) ) @@ -56,6 +59,7 @@ data=zeros_data, thickness=8, colors="w", + uniform_color=False, # initialize with same color across vertices, but we will change the per-vertex colors later offset=(0, 10, 0) ) diff --git a/examples/line/line_dataslice.py b/examples/line/line_dataslice.py index 6ef9d0d90..def3f678f 100644 --- a/examples/line/line_dataslice.py +++ b/examples/line/line_dataslice.py @@ -30,11 +30,11 @@ sine = figure[0, 0].add_line(data=sine_data, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine = figure[0, 0].add_line(data=cosine_data, thickness=12, cmap="autumn") +cosine = figure[0, 0].add_line(data=cosine_data, thickness=12, cmap="autumn", uniform_color=False) # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc = figure[0, 0].add_line(data=sinc_data, thickness=5, colors=colors) +sinc = figure[0, 0].add_line(data=sinc_data, thickness=5, colors=colors, uniform_color=False) figure.show() diff --git a/examples/line_collection/line_collection_slicing.py b/examples/line_collection/line_collection_slicing.py index f829a53c6..022ca094a 100644 --- a/examples/line_collection/line_collection_slicing.py +++ b/examples/line_collection/line_collection_slicing.py @@ -26,6 +26,7 @@ multi_data, thickness=[2, 10, 2, 5, 5, 5, 8, 8, 8, 9, 3, 3, 3, 4, 4], separation=4, + uniform_color=False, metadatas=list(range(15)), # some metadata names=list("abcdefghijklmno"), # unique name for each line ) diff --git a/examples/scatter/scatter.py b/examples/scatter/scatter.py index 838199ecb..973a55512 100644 --- a/examples/scatter/scatter.py +++ b/examples/scatter/scatter.py @@ -36,7 +36,7 @@ colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points # use an alpha value since this will be a lot of points -figure[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) +figure[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, uniform_color=False, alpha=0.6) figure.show() diff --git a/examples/scatter/scatter_cmap.py b/examples/scatter/scatter_cmap.py index 3c7bd0e21..24f5e00ab 100644 --- a/examples/scatter/scatter_cmap.py +++ b/examples/scatter/scatter_cmap.py @@ -36,7 +36,7 @@ colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points # use an alpha value since this will be a lot of points -figure[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) +figure[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, uniform_color=False, alpha=0.6) figure.show() diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 73520cc84..1b639a127 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -23,7 +23,7 @@ def data(self) -> VertexPositions: @data.setter def data(self, value): - self._data[:] = value + self._data.set_value(self, value) @property def colors(self) -> VertexColors | pygfx.Color: @@ -36,11 +36,7 @@ def colors(self) -> VertexColors | pygfx.Color: @colors.setter def colors(self, value: str | np.ndarray | Sequence[float] | Sequence[str]): - if isinstance(self._colors, VertexColors): - self._colors[:] = value - - elif isinstance(self._colors, UniformColor): - self._colors.set_value(self, value) + self._colors.set_value(self, value) @property def cmap(self) -> VertexCmap: @@ -75,10 +71,9 @@ def __init__( self, data: Any, colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", - uniform_color: bool = False, + uniform_color: bool = True, cmap: str | VertexCmap = None, cmap_transform: np.ndarray = None, - isolated_buffer: bool = True, size_space: str = "screen", *args, **kwargs, @@ -86,7 +81,7 @@ def __init__( if isinstance(data, VertexPositions): self._data = data else: - self._data = VertexPositions(data, isolated_buffer=isolated_buffer) + self._data = VertexPositions(data) if cmap_transform is not None and cmap is None: raise ValueError("must pass `cmap` if passing `cmap_transform`") @@ -94,7 +89,7 @@ def __init__( if cmap is not None: # if a cmap is specified it overrides colors argument if uniform_color: - raise TypeError("Cannot use cmap if uniform_color=True") + raise TypeError("Cannot use `cmap` if `uniform_color=True`, pass `uniform_color=False` to use `cmap`.") if isinstance(cmap, str): # make colors from cmap @@ -132,7 +127,7 @@ def __init__( if not isinstance(colors, str): # not a single color if not len(colors) in [3, 4]: # not an RGB(A) array raise TypeError( - "must pass a single color if using `uniform_colors=True`" + "Must pass `uniform_colors=False` if using multiple colors" ) self._colors = UniformColor(colors) self._cmap = None diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index 779310476..05b9da6d7 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -1,5 +1,5 @@ from warnings import warn -from typing import Literal +from typing import Callable import numpy as np from numpy.typing import NDArray @@ -78,7 +78,7 @@ def block_events(self, val: bool): """ self._block_events = val - def add_event_handler(self, handler: callable): + def add_event_handler(self, handler: Callable): """ Add an event handler. All added event handlers are called when this feature changes. @@ -89,7 +89,7 @@ def add_event_handler(self, handler: callable): Parameters ---------- - handler: callable + handler: Callable a function to call when this feature changes """ @@ -102,7 +102,7 @@ def add_event_handler(self, handler: callable): self._event_handlers.append(handler) - def remove_event_handler(self, handler: callable): + def remove_event_handler(self, handler: Callable): """ Remove a registered event ``handler``. @@ -138,31 +138,22 @@ class BufferManager(GraphicFeature): def __init__( self, data: NDArray | pygfx.Buffer, - buffer_type: Literal["buffer", "texture", "texture-array"] = "buffer", - isolated_buffer: bool = True, **kwargs, ): super().__init__(**kwargs) - if isolated_buffer and not isinstance(data, pygfx.Resource): - # useful if data is read-only, example: memmaps - bdata = np.zeros(data.shape, dtype=data.dtype) - bdata[:] = data[:] - else: - # user's input array is used as the buffer - bdata = data if isinstance(data, pygfx.Resource): # already a buffer, probably used for # managing another BufferManager, example: VertexCmap manages VertexColors self._buffer = data - elif buffer_type == "buffer": - self._buffer = pygfx.Buffer(bdata) else: - raise ValueError( - "`data` must be a pygfx.Buffer instance or `buffer_type` must be one of: 'buffer' or 'texture'" - ) + # create a buffer + bdata = np.empty(data.shape, dtype=data.dtype) + bdata[:] = data[:] + + self._buffer = pygfx.Buffer(bdata) - self._event_handlers: list[callable] = list() + self._event_handlers: list[Callable] = list() @property def value(self) -> np.ndarray: @@ -174,7 +165,7 @@ def set_value(self, graphic, value): self[:] = value @property - def buffer(self) -> pygfx.Buffer | pygfx.Texture: + def buffer(self) -> pygfx.Buffer: """managed buffer""" return self._buffer diff --git a/fastplotlib/graphics/features/_image.py b/fastplotlib/graphics/features/_image.py index 648f79bc8..cb66bb1ef 100644 --- a/fastplotlib/graphics/features/_image.py +++ b/fastplotlib/graphics/features/_image.py @@ -33,7 +33,7 @@ class TextureArray(GraphicFeature): }, ] - def __init__(self, data, isolated_buffer: bool = True, property_name: str = "data"): + def __init__(self, data, property_name: str = "data"): super().__init__(property_name=property_name) data = self._fix_data(data) @@ -41,13 +41,9 @@ def __init__(self, data, isolated_buffer: bool = True, property_name: str = "dat shared = pygfx.renderers.wgpu.get_shared() self._texture_limit_2d = shared.device.limits["max-texture-dimension-2d"] - if isolated_buffer: - # useful if data is read-only, example: memmaps - self._value = np.zeros(data.shape, dtype=data.dtype) - self.value[:] = data[:] - else: - # user's input array is used as the buffer - self._value = data + # create a new buffer + self._value = np.zeros(data.shape, dtype=data.dtype) + self.value[:] = data[:] # data start indices for each Texture self._row_indices = np.arange( diff --git a/fastplotlib/graphics/features/_mesh.py b/fastplotlib/graphics/features/_mesh.py index 7355acb4e..776d77ce4 100644 --- a/fastplotlib/graphics/features/_mesh.py +++ b/fastplotlib/graphics/features/_mesh.py @@ -51,18 +51,14 @@ class MeshIndices(VertexPositions): }, ] - def __init__( - self, data: Any, isolated_buffer: bool = True, property_name: str = "indices" - ): + def __init__(self, data: Any, 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 - ) + super().__init__(data, property_name=property_name) def _fix_data(self, data): if data.ndim != 2 or data.shape[1] not in (3, 4): diff --git a/fastplotlib/graphics/features/_positions.py b/fastplotlib/graphics/features/_positions.py index 295d22417..1187b4c88 100644 --- a/fastplotlib/graphics/features/_positions.py +++ b/fastplotlib/graphics/features/_positions.py @@ -39,7 +39,6 @@ def __init__( self, colors: str | pygfx.Color | np.ndarray | Sequence[float] | Sequence[str], n_colors: int, - isolated_buffer: bool = True, property_name: str = "colors", ): """ @@ -57,9 +56,55 @@ def __init__( """ data = parse_colors(colors, n_colors) - super().__init__( - data=data, isolated_buffer=isolated_buffer, property_name=property_name - ) + super().__init__(data=data, property_name=property_name) + + def set_value( + self, + graphic, + value: str | pygfx.Color | np.ndarray | Sequence[float] | Sequence[str], + ): + """set the entire array, create new buffer if necessary""" + if isinstance(value, (np.ndarray, list, tuple)): + # TODO: Refactor this triage so it's more elegant + + # first make sure it's not representing one color + skip = False + if isinstance(value, np.ndarray): + if (value.shape in ((3,), (4,))) and (np.issubdtype(value.dtype, np.floating) or np.issubdtype(value.dtype, np.integer)): + # represents one color + skip = True + elif isinstance(value, (list, tuple)): + if len(value) in (3, 4) and all([isinstance(v, (float, int)) for v in value]): + # represents one color + skip = True + + # check if the number of elements matches current buffer size + if not skip and self.buffer.data.shape[0] != len(value): + # parse the new colors + new_colors = parse_colors(value, len(value)) + + # destroy old buffer + if self._buffer._wgpu_object is not None: + self._buffer._wgpu_object.destroy() + + # create new buffer + self._buffer = pygfx.Buffer(new_colors) + graphic.world_object.geometry.colors = self.buffer + + if len(self._event_handlers) < 1: + return + + event_info = { + "key": slice(None), + "value": new_colors, + "user_value": value, + } + + event = GraphicFeatureEvent(self._property_name, info=event_info) + self._call_event_handlers(event) + return + + self[:] = value @block_reentrance def __setitem__( @@ -231,18 +276,14 @@ class VertexPositions(BufferManager): }, ] - def __init__( - self, data: Any, isolated_buffer: bool = True, property_name: str = "data" - ): + def __init__(self, data: Any, property_name: str = "data"): """ Manages the vertex positions 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 - ) + super().__init__(data, property_name=property_name) def _fix_data(self, data): if data.ndim == 1: @@ -261,6 +302,39 @@ def _fix_data(self, data): return to_gpu_supported_dtype(data) + def set_value(self, graphic, value): + """Sets the entire array, creates new buffer if necessary""" + if isinstance(value, np.ndarray): + if self.buffer.data.shape[0] != value.shape[0]: + # number of items doesn't match, create a new buffer + + # if data is not 3D + if value.ndim == 1: + # this is already a newly allocated buffer + # _fix_data creates a new array so we don't need to re-allocate with np.zeros + bdata = self._fix_data(value) + + elif value.shape[1] == 2: + # this is already a newly allocated buffer + bdata = self._fix_data(value) + + elif value.shape[1] == 3: + # need to allocate a buffer to use here + bdata = np.empty(value.shape, dtype=np.float32) + bdata[:] = value[:] + + # destroy old buffer + if self._buffer._wgpu_object is not None: + self._buffer._wgpu_object.destroy() + + # create the new buffer + self._buffer = pygfx.Buffer(bdata) + graphic.world_object.geometry.positions = self.buffer + self._emit_event(self._property_name, key=slice(None), value=value) + return + + self[:] = value + @block_reentrance def __setitem__( self, diff --git a/fastplotlib/graphics/features/_scatter.py b/fastplotlib/graphics/features/_scatter.py index 16671ef89..ce10fe4a8 100644 --- a/fastplotlib/graphics/features/_scatter.py +++ b/fastplotlib/graphics/features/_scatter.py @@ -100,6 +100,37 @@ def searchsorted_markers_to_int_array(markers_str_array: np.ndarray[str]): return marker_int_searchsorted_vals[indices] +def parse_markers_init(markers: str | Sequence[str] | np.ndarray, n_datapoints: int): + # first validate then allocate buffers + + if isinstance(markers, str): + markers = user_input_to_marker(markers) + + elif isinstance(markers, (tuple, list, np.ndarray)): + validate_user_markers_array(markers) + + # allocate buffers + markers_int_array = np.zeros(n_datapoints, dtype=np.int32) + + marker_str_length = max(map(len, list(pygfx.MarkerShape))) + + markers_readable_array = np.empty(n_datapoints, dtype=f" np.ndarray[str]: @@ -200,6 +200,29 @@ def _set_markers_arrays(self, key, value, n_markers): "new markers value must be a str, Sequence or np.ndarray of new marker values" ) + def set_value(self, graphic, value): + """set all the markers, create new buffer if necessary""" + if isinstance(value, (np.ndarray, list, tuple)): + if self.buffer.data.shape[0] != len(value): + # need to create a new buffer + markers_int_array, self._markers_readable_array = parse_markers_init( + value, len(value) + ) + + # destroy old buffer + if self._buffer._wgpu_object is not None: + self._buffer._wgpu_object.destroy() + + # set new buffer + self._buffer = pygfx.Buffer(markers_int_array) + graphic.geometry.markers = self.buffer + + self._emit_event(self._property_name, key=slice(None), value=value) + + return + + self[:] = value + @block_reentrance def __setitem__( self, @@ -414,18 +437,15 @@ def __init__( self, rotations: int | float | np.ndarray | Sequence[int | float], n_datapoints: int, - isolated_buffer: bool = True, property_name: str = "point_rotations", ): """ Manages rotations buffer of scatter points. """ - sizes = self._fix_sizes(rotations, n_datapoints) - super().__init__( - data=sizes, isolated_buffer=isolated_buffer, property_name=property_name - ) + sizes = self._fix_rotations(rotations, n_datapoints) + super().__init__(data=sizes, property_name=property_name) - def _fix_sizes( + def _fix_rotations( self, sizes: int | float | np.ndarray | Sequence[int | float], n_datapoints: int, @@ -454,6 +474,26 @@ def _fix_sizes( return sizes + def set_value(self, graphic, value): + """set all rotations, create new buffer if necessary""" + if isinstance(value, (np.ndarray, list, tuple)): + if self.buffer.data.shape[0] != value.shape[0]: + # need to create a new buffer + value = self._fix_rotations(value, len(value)) + data = np.empty(shape=(len(value),), dtype=np.float32) + + # destroy old buffer + if self._buffer._wgpu_object is not None: + self._buffer._wgpu_object.destroy() + + # set new buffer + self._buffer = pygfx.Buffer(data) + graphic.world_object.geometry.rotations = self.buffer + self._emit_event(self._property_name, key=slice(None), value=value) + return + + self[:] = value + @block_reentrance def __setitem__( self, @@ -488,16 +528,13 @@ def __init__( self, sizes: int | float | np.ndarray | Sequence[int | float], n_datapoints: int, - isolated_buffer: bool = True, property_name: str = "sizes", ): """ Manages sizes buffer of scatter points. """ sizes = self._fix_sizes(sizes, n_datapoints) - super().__init__( - data=sizes, isolated_buffer=isolated_buffer, property_name=property_name - ) + super().__init__(data=sizes, property_name=property_name) def _fix_sizes( self, @@ -533,6 +570,27 @@ def _fix_sizes( return sizes + def set_value(self, graphic, value): + """set all sizes, create new buffer if necessary""" + if isinstance(value, (np.ndarray, list, tuple)): + if self.buffer.data.shape[0] != len(value): + # create new buffer + value = self._fix_sizes(value, len(value)) + data = np.empty(shape=(len(value),), dtype=np.float32) + + # destroy old buffer + if self._buffer._wgpu_object is not None: + self._buffer._wgpu_object.destroy() + + # set new buffer + self._buffer = pygfx.Buffer(data) + graphic.geometry.sizes = self.buffer + + self._emit_event(self._property_name, key=slice(None), value=value) + return + + self[:] = value + @block_reentrance def __setitem__( self, diff --git a/fastplotlib/graphics/features/_vectors.py b/fastplotlib/graphics/features/_vectors.py index 9c86d25fc..729562b06 100644 --- a/fastplotlib/graphics/features/_vectors.py +++ b/fastplotlib/graphics/features/_vectors.py @@ -22,7 +22,6 @@ class VectorPositions(GraphicFeature): def __init__( self, positions: np.ndarray, - isolated_buffer: bool = True, property_name: str = "positions", ): """ @@ -111,7 +110,6 @@ class VectorDirections(GraphicFeature): 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""" diff --git a/fastplotlib/graphics/features/_volume.py b/fastplotlib/graphics/features/_volume.py index ec4c4052a..532065fb7 100644 --- a/fastplotlib/graphics/features/_volume.py +++ b/fastplotlib/graphics/features/_volume.py @@ -34,7 +34,7 @@ class TextureArrayVolume(GraphicFeature): }, ] - def __init__(self, data, isolated_buffer: bool = True): + def __init__(self, data): super().__init__(property_name="data") data = self._fix_data(data) @@ -43,13 +43,9 @@ def __init__(self, data, isolated_buffer: bool = True): self._texture_size_limit = shared.device.limits["max-texture-dimension-3d"] - if isolated_buffer: - # useful if data is read-only, example: memmaps - self._value = np.zeros(data.shape, dtype=data.dtype) - self.value[:] = data[:] - else: - # user's input array is used as the buffer - self._value = data + # create a new buffer that will be used for the texture data + self._value = np.zeros(data.shape, dtype=data.dtype) + self.value[:] = data[:] # data start indices for each Texture self._row_indices = np.arange( diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 1eaf54bb6..291128f04 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -1,6 +1,7 @@ import math from typing import * +import numpy as np import pygfx from ..utils import quick_min_max @@ -93,7 +94,6 @@ def __init__( cmap: str = "plasma", interpolation: str = "nearest", cmap_interpolation: str = "linear", - isolated_buffer: bool = True, **kwargs, ): """ @@ -121,12 +121,6 @@ def __init__( cmap_interpolation: str, optional, default "linear" colormap interpolation method, one of "nearest" or "linear" - isolated_buffer: bool, default True - If True, initialize a buffer with the same shape as the input data and then - set the data, useful if the data arrays are ready-only such as memmaps. - If False, the input array is itself used as the buffer - useful if the - array is large. - kwargs: additional keyword arguments passed to :class:`.Graphic` @@ -134,7 +128,7 @@ def __init__( super().__init__(**kwargs) - world_object = pygfx.Group() + group = pygfx.Group() if isinstance(data, TextureArray): # share buffer @@ -142,7 +136,7 @@ def __init__( else: # create new texture array to manage buffer # texture array that manages the multiple textures on the GPU that represent this image - self._data = TextureArray(data, isolated_buffer=isolated_buffer) + self._data = TextureArray(data) if (vmin is None) or (vmax is None): _vmin, _vmax = quick_min_max(self.data.value) @@ -156,6 +150,7 @@ def __init__( self._vmax = ImageVmax(vmax) self._interpolation = ImageInterpolation(interpolation) + self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation) # set map to None for RGB images if self._data.value.ndim > 2: @@ -164,7 +159,6 @@ def __init__( else: # use TextureMap for grayscale images self._cmap = ImageCmap(cmap) - self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation) _map = pygfx.TextureMap( self._cmap.texture, @@ -180,6 +174,14 @@ def __init__( pick_write=True, ) + # create the _ImageTile world objects, add to group + for tile in self._create_tiles(): + group.add(tile) + + self._set_world_object(group) + + def _create_tiles(self) -> list[_ImageTile]: + tiles = list() # iterate through each texture chunk and create # an _ImageTile, offset the tile using the data indices for texture, chunk_index, data_slice in self._data: @@ -200,9 +202,9 @@ def __init__( img.world.x = data_col_start img.world.y = data_row_start - world_object.add(img) + tiles.append(img) - self._set_world_object(world_object) + return tiles @property def data(self) -> TextureArray: @@ -211,6 +213,39 @@ def data(self) -> TextureArray: @data.setter def data(self, data): + if isinstance(data, np.ndarray): + # check if a new buffer is required + if self._data.value.shape != data.shape: + # create new TextureArray + self._data = TextureArray(data) + + # cmap based on if rgb or grayscale + if self._data.value.ndim > 2: + self._cmap = None + + # must be None if RGB(A) + self._material.map = None + else: + if self.cmap is None: # have switched from RGBA -> grayscale image + # create default cmap + self._cmap = ImageCmap("plasma") + self._material.map = pygfx.TextureMap( + self._cmap.texture, + filter=self._cmap_interpolation.value, + wrap="clamp-to-edge", + ) + + self._material.clim = quick_min_max(self.data.value) + + # clear image tiles + self.world_object.clear() + + # create new tiles + for tile in self._create_tiles(): + self.world_object.add(tile) + + return + self._data[:] = data @property diff --git a/fastplotlib/graphics/image_volume.py b/fastplotlib/graphics/image_volume.py index db616b30d..c41c19eab 100644 --- a/fastplotlib/graphics/image_volume.py +++ b/fastplotlib/graphics/image_volume.py @@ -113,7 +113,6 @@ def __init__( substep_size: float = 0.1, emissive: str | tuple | np.ndarray = (0, 0, 0), shininess: int = 30, - isolated_buffer: bool = True, **kwargs, ): """ @@ -170,11 +169,6 @@ def __init__( How shiny the specular highlight is; a higher value gives a sharper highlight. Used only if `mode` = "iso" - isolated_buffer: bool, default True - If True, initialize a buffer with the same shape as the input data and then set the data, useful if the - data arrays are ready-only such as memmaps. If False, the input array is itself used as the - buffer - useful if the array is large. - kwargs additional keyword arguments passed to :class:`.Graphic` @@ -188,7 +182,7 @@ def __init__( super().__init__(**kwargs) - world_object = pygfx.Group() + group = pygfx.Group() if isinstance(data, TextureArrayVolume): # share existing buffer @@ -196,7 +190,7 @@ def __init__( else: # create new texture array to manage buffer # texture array that manages the textures on the GPU that represent this image volume - self._data = TextureArrayVolume(data, isolated_buffer=isolated_buffer) + self._data = TextureArrayVolume(data) if (vmin is None) or (vmax is None): _vmin, _vmax = quick_min_max(self.data.value) @@ -237,6 +231,15 @@ def __init__( self._mode = VolumeRenderMode(mode) + # create tiles + for tile in self._create_tiles(): + group.add(tile) + + self._set_world_object(group) + + def _create_tiles(self) -> list[_VolumeTile]: + tiles = list() + # iterate through each texture chunk and create # a _VolumeTile, offset the tile using the data indices for texture, chunk_index, data_slice in self._data: @@ -259,9 +262,9 @@ def __init__( vol.world.x = data_col_start vol.world.y = data_row_start - world_object.add(vol) + tiles.append(vol) - self._set_world_object(world_object) + return tiles @property def data(self) -> TextureArrayVolume: @@ -270,6 +273,21 @@ def data(self) -> TextureArrayVolume: @data.setter def data(self, data): + if isinstance(data, np.ndarray): + # check if a new buffer is required + if self._data.value.shape != data.shape: + # create new TextureArray + self._data = TextureArrayVolume(data) + + # clear image tiles + self.world_object.clear() + + # create new tiles + for tile in self._create_tiles(): + self.world_object.add(tile) + + return + self._data[:] = data @property diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index f2d862067..93d1aa4c1 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -36,10 +36,9 @@ def __init__( data: Any, thickness: float = 2.0, colors: str | np.ndarray | Sequence = "w", - uniform_color: bool = False, + uniform_color: bool = True, cmap: str = None, cmap_transform: np.ndarray | Sequence = None, - isolated_buffer: bool = True, size_space: str = "screen", **kwargs, ): @@ -61,9 +60,10 @@ def __init__( specify colors as a single human-readable string, a single RGBA array, or a Sequence (array, tuple, or list) of strings or RGBA arrays - uniform_color: bool, default ``False`` - if True, uses a uniform buffer for the line color, - basically saves GPU VRAM when the entire line has a single color + uniform_color: bool, default ``True`` + if ``True``, uses a uniform buffer for the line color, + basically saves GPU VRAM when the entire line has a single color. + If ``False``, you can set per-vertex colors. cmap: str, optional Apply a colormap to the line instead of assigning colors manually, this @@ -87,7 +87,6 @@ def __init__( uniform_color=uniform_color, cmap=cmap, cmap_transform=cmap_transform, - isolated_buffer=isolated_buffer, size_space=size_space, **kwargs, ) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 275cc1e47..8ef1c4d8e 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -128,14 +128,13 @@ def __init__( data: np.ndarray | List[np.ndarray], thickness: float | Sequence[float] = 2.0, colors: str | Sequence[str] | np.ndarray | Sequence[np.ndarray] = "w", - uniform_colors: bool = False, + uniform_color: bool = True, cmap: Sequence[str] | str = None, cmap_transform: np.ndarray | List = None, name: str = None, names: list[str] = None, metadata: Any = None, metadatas: Sequence[Any] | np.ndarray = None, - isolated_buffer: bool = True, kwargs_lines: list[dict] = None, **kwargs, ): @@ -320,11 +319,10 @@ def __init__( data=d, thickness=_s, colors=_c, - uniform_color=uniform_colors, + uniform_color=uniform_color, cmap=_cmap, name=_name, metadata=_m, - isolated_buffer=isolated_buffer, **kwargs_lines, ) @@ -560,7 +558,6 @@ def __init__( names: list[str] = None, metadata: Any = None, metadatas: Sequence[Any] | np.ndarray = None, - isolated_buffer: bool = True, separation: float = 10.0, separation_axis: str = "y", kwargs_lines: list[dict] = None, @@ -634,7 +631,6 @@ def __init__( names=names, metadata=metadata, metadatas=metadatas, - isolated_buffer=isolated_buffer, kwargs_lines=kwargs_lines, **kwargs, ) diff --git a/fastplotlib/graphics/mesh.py b/fastplotlib/graphics/mesh.py index 2e5a11851..bd4eea343 100644 --- a/fastplotlib/graphics/mesh.py +++ b/fastplotlib/graphics/mesh.py @@ -38,7 +38,6 @@ def __init__( mapcoords: Any = None, cmap: str | dict | pygfx.Texture | pygfx.TextureMap | np.ndarray = None, clim: tuple[float, float] = None, - isolated_buffer: bool = True, **kwargs, ): """ @@ -77,12 +76,6 @@ def __init__( Both 1D and 2D colormaps are supported, though the mapcoords has to match the dimensionality. An image can also be used, this is basically a 2D colormap. - isolated_buffer: bool, default True - If True, initialize a buffer with the same shape as the input data and then - set the data, useful if the data arrays are ready-only such as memmaps. - If False, the input array is itself used as the buffer - useful if the - array is large. In almost all cases this should be ``True``. - **kwargs passed to :class:`.Graphic` @@ -93,16 +86,12 @@ def __init__( if isinstance(positions, VertexPositions): self._positions = positions else: - self._positions = VertexPositions( - positions, isolated_buffer=isolated_buffer, property_name="positions" - ) + self._positions = VertexPositions(positions, property_name="positions") if isinstance(positions, MeshIndices): self._indices = indices else: - self._indices = MeshIndices( - indices, isolated_buffer=isolated_buffer, property_name="indices" - ) + self._indices = MeshIndices(indices, property_name="indices") self._cmap = MeshCmap(cmap) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index a2e696a82..0464fd528 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -40,12 +40,12 @@ def __init__( self, data: Any, colors: str | np.ndarray | Sequence[float] | Sequence[str] = "w", - uniform_color: bool = False, + uniform_color: bool = True, cmap: str = None, cmap_transform: np.ndarray = None, mode: Literal["markers", "simple", "gaussian", "image"] = "markers", markers: str | np.ndarray | Sequence[str] = "o", - uniform_marker: bool = False, + uniform_marker: bool = True, custom_sdf: str = None, edge_colors: str | np.ndarray | pygfx.Color | Sequence[float] = "black", uniform_edge_color: bool = True, @@ -56,7 +56,6 @@ def __init__( sizes: float | np.ndarray | Sequence[float] = 1, uniform_size: bool = False, size_space: str = "screen", - isolated_buffer: bool = True, **kwargs, ): """ @@ -72,14 +71,15 @@ def __init__( specify colors as a single human-readable string, a single RGBA array, or a Sequence (array, tuple, or list) of strings or RGBA arrays - uniform_color: bool, default False - if True, uses a uniform buffer for the scatter point colors. Useful if you need to - save GPU VRAM when all points have the same color. + uniform_color: bool, default ``True`` + if ``True``, uses a uniform buffer for the scatter point colors. Useful if you need to + save GPU VRAM when all points have the same color. If ``False``, you can set per-vertex colors. cmap: str, optional apply a colormap to the scatter instead of assigning colors manually, this - overrides any argument passed to "colors". For supported colormaps see the - ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + overrides any argument passed to "colors". + For supported colormaps see the ``cmap`` library catalogue: + https://cmap-docs.readthedocs.io/en/stable/catalog/ cmap_transform: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap @@ -103,9 +103,10 @@ def __init__( * Emojis: "❤️♠️♣️♦️💎💍✳️📍". * A string containing the value "custom". In this case, WGSL code defined by ``custom_sdf`` will be used. - uniform_marker: bool, default False - Use the same marker for all points. Only valid when `mode` is "markers". Useful if you need to use - the same marker for all points and want to save GPU RAM. + uniform_marker: bool, default ``True`` + If ``True``, use the same marker for all points. Only valid when `mode` is "markers". + Useful if you need to use the same marker for all points and want to save GPU RAM. If ``False``, you can + set per-vertex markers. custom_sdf: str = None, The SDF code for the marker shape when the marker is set to custom. @@ -125,8 +126,9 @@ def __init__( edge_colors: str | np.ndarray | pygfx.Color | Sequence[float], default "black" edge color of the markers, used when `mode` is "markers" - uniform_edge_color: bool, default True - Set the same edge color for all markers. Useful for saving GPU RAM. + uniform_edge_color: bool, default ``True`` + Set the same edge color for all markers. Useful for saving GPU RAM. Set to ``False`` for per-vertex edge + colors edge_width: float = 1.0, Width of the marker edges. used when `mode` is "markers". @@ -147,17 +149,13 @@ def __init__( sizes: float or iterable of float, optional, default 1.0 sizes of the scatter points - uniform_size: bool, default False - if True, uses a uniform buffer for the scatter point sizes. Useful if you need to - save GPU VRAM when all points have the same size. + uniform_size: bool, default ``False`` + if ``True``, uses a uniform buffer for the scatter point sizes. Useful if you need to + save GPU VRAM when all points have the same size. Set to ``False`` if you need per-vertex sizes. size_space: str, default "screen" coordinate space in which the size is expressed, one of ("screen", "world", "model") - isolated_buffer: bool, default True - whether the buffers should be isolated from the user input array. - Generally always ``True``, ``False`` is for rare advanced use if you have large arrays. - kwargs passed to :class:`.Graphic` @@ -169,7 +167,6 @@ def __init__( uniform_color=uniform_color, cmap=cmap, cmap_transform=cmap_transform, - isolated_buffer=isolated_buffer, size_space=size_space, **kwargs, ) diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 06a4c7517..b6610f456 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -33,7 +33,6 @@ def add_image( cmap: str = "plasma", interpolation: str = "nearest", cmap_interpolation: str = "linear", - isolated_buffer: bool = True, **kwargs, ) -> ImageGraphic: """ @@ -62,12 +61,6 @@ def add_image( cmap_interpolation: str, optional, default "linear" colormap interpolation method, one of "nearest" or "linear" - isolated_buffer: bool, default True - If True, initialize a buffer with the same shape as the input data and then - set the data, useful if the data arrays are ready-only such as memmaps. - If False, the input array is itself used as the buffer - useful if the - array is large. - kwargs: additional keyword arguments passed to :class:`.Graphic` @@ -81,7 +74,6 @@ def add_image( cmap, interpolation, cmap_interpolation, - isolated_buffer, **kwargs, ) @@ -100,7 +92,6 @@ def add_image_volume( substep_size: float = 0.1, emissive: str | tuple | numpy.ndarray = (0, 0, 0), shininess: int = 30, - isolated_buffer: bool = True, **kwargs, ) -> ImageVolumeGraphic: """ @@ -158,11 +149,6 @@ def add_image_volume( How shiny the specular highlight is; a higher value gives a sharper highlight. Used only if `mode` = "iso" - isolated_buffer: bool, default True - If True, initialize a buffer with the same shape as the input data and then set the data, useful if the - data arrays are ready-only such as memmaps. If False, the input array is itself used as the - buffer - useful if the array is large. - kwargs additional keyword arguments passed to :class:`.Graphic` @@ -183,7 +169,6 @@ def add_image_volume( substep_size, emissive, shininess, - isolated_buffer, **kwargs, ) @@ -199,7 +184,6 @@ def add_line_collection( names: list[str] = None, metadata: Any = None, metadatas: Union[Sequence[Any], numpy.ndarray] = None, - isolated_buffer: bool = True, kwargs_lines: list[dict] = None, **kwargs, ) -> LineCollection: @@ -268,7 +252,6 @@ def add_line_collection( names, metadata, metadatas, - isolated_buffer, kwargs_lines, **kwargs, ) @@ -278,10 +261,9 @@ def add_line( data: Any, thickness: float = 2.0, colors: Union[str, numpy.ndarray, Sequence] = "w", - uniform_color: bool = False, + uniform_color: bool = True, cmap: str = None, cmap_transform: Union[numpy.ndarray, Sequence] = None, - isolated_buffer: bool = True, size_space: str = "screen", **kwargs, ) -> LineGraphic: @@ -304,9 +286,10 @@ def add_line( specify colors as a single human-readable string, a single RGBA array, or a Sequence (array, tuple, or list) of strings or RGBA arrays - uniform_color: bool, default ``False`` - if True, uses a uniform buffer for the line color, - basically saves GPU VRAM when the entire line has a single color + uniform_color: bool, default ``True`` + if ``True``, uses a uniform buffer for the line color, + basically saves GPU VRAM when the entire line has a single color. + If ``False``, you can set per-vertex colors. cmap: str, optional Apply a colormap to the line instead of assigning colors manually, this @@ -332,7 +315,6 @@ def add_line( uniform_color, cmap, cmap_transform, - isolated_buffer, size_space, **kwargs, ) @@ -348,7 +330,6 @@ def add_line_stack( names: list[str] = None, metadata: Any = None, metadatas: Union[Sequence[Any], numpy.ndarray] = None, - isolated_buffer: bool = True, separation: float = 10.0, separation_axis: str = "y", kwargs_lines: list[dict] = None, @@ -425,7 +406,6 @@ def add_line_stack( names, metadata, metadatas, - isolated_buffer, separation, separation_axis, kwargs_lines, @@ -448,7 +428,6 @@ def add_mesh( | numpy.ndarray ) = None, clim: tuple[float, float] = None, - isolated_buffer: bool = True, **kwargs, ) -> MeshGraphic: """ @@ -488,12 +467,6 @@ def add_mesh( Both 1D and 2D colormaps are supported, though the mapcoords has to match the dimensionality. An image can also be used, this is basically a 2D colormap. - isolated_buffer: bool, default True - If True, initialize a buffer with the same shape as the input data and then - set the data, useful if the data arrays are ready-only such as memmaps. - If False, the input array is itself used as the buffer - useful if the - array is large. In almost all cases this should be ``True``. - **kwargs passed to :class:`.Graphic` @@ -509,7 +482,6 @@ def add_mesh( mapcoords, cmap, clim, - isolated_buffer, **kwargs, ) @@ -574,12 +546,12 @@ def add_scatter( self, data: Any, colors: Union[str, numpy.ndarray, Sequence[float], Sequence[str]] = "w", - uniform_color: bool = False, + uniform_color: bool = True, cmap: str = None, cmap_transform: numpy.ndarray = None, mode: Literal["markers", "simple", "gaussian", "image"] = "markers", markers: Union[str, numpy.ndarray, Sequence[str]] = "o", - uniform_marker: bool = False, + uniform_marker: bool = True, custom_sdf: str = None, edge_colors: Union[ str, pygfx.utils.color.Color, numpy.ndarray, Sequence[float] @@ -592,7 +564,6 @@ def add_scatter( sizes: Union[float, numpy.ndarray, Sequence[float]] = 1, uniform_size: bool = False, size_space: str = "screen", - isolated_buffer: bool = True, **kwargs, ) -> ScatterGraphic: """ @@ -609,14 +580,15 @@ def add_scatter( specify colors as a single human-readable string, a single RGBA array, or a Sequence (array, tuple, or list) of strings or RGBA arrays - uniform_color: bool, default False - if True, uses a uniform buffer for the scatter point colors. Useful if you need to - save GPU VRAM when all points have the same color. + uniform_color: bool, default ``True`` + if ``True``, uses a uniform buffer for the scatter point colors. Useful if you need to + save GPU VRAM when all points have the same color. If ``False``, you can set per-vertex colors. cmap: str, optional apply a colormap to the scatter instead of assigning colors manually, this - overrides any argument passed to "colors". For supported colormaps see the - ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + overrides any argument passed to "colors". + For supported colormaps see the ``cmap`` library catalogue: + https://cmap-docs.readthedocs.io/en/stable/catalog/ cmap_transform: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap @@ -640,9 +612,10 @@ def add_scatter( * Emojis: "❤️♠️♣️♦️💎💍✳️📍". * A string containing the value "custom". In this case, WGSL code defined by ``custom_sdf`` will be used. - uniform_marker: bool, default False - Use the same marker for all points. Only valid when `mode` is "markers". Useful if you need to use - the same marker for all points and want to save GPU RAM. + uniform_marker: bool, default ``True`` + If ``True``, use the same marker for all points. Only valid when `mode` is "markers". + Useful if you need to use the same marker for all points and want to save GPU RAM. If ``False``, you can + set per-vertex markers. custom_sdf: str = None, The SDF code for the marker shape when the marker is set to custom. @@ -662,8 +635,9 @@ def add_scatter( edge_colors: str | np.ndarray | pygfx.Color | Sequence[float], default "black" edge color of the markers, used when `mode` is "markers" - uniform_edge_color: bool, default True - Set the same edge color for all markers. Useful for saving GPU RAM. + uniform_edge_color: bool, default ``True`` + Set the same edge color for all markers. Useful for saving GPU RAM. Set to ``False`` for per-vertex edge + colors edge_width: float = 1.0, Width of the marker edges. used when `mode` is "markers". @@ -684,17 +658,13 @@ def add_scatter( sizes: float or iterable of float, optional, default 1.0 sizes of the scatter points - uniform_size: bool, default False - if True, uses a uniform buffer for the scatter point sizes. Useful if you need to - save GPU VRAM when all points have the same size. + uniform_size: bool, default ``False`` + if ``True``, uses a uniform buffer for the scatter point sizes. Useful if you need to + save GPU VRAM when all points have the same size. Set to ``False`` if you need per-vertex sizes. size_space: str, default "screen" coordinate space in which the size is expressed, one of ("screen", "world", "model") - isolated_buffer: bool, default True - whether the buffers should be isolated from the user input array. - Generally always ``True``, ``False`` is for rare advanced use if you have large arrays. - kwargs passed to :class:`.Graphic` @@ -720,7 +690,6 @@ def add_scatter( sizes, uniform_size, size_space, - isolated_buffer, **kwargs, )