From cd49cd257db8ed9d36af57f976fe7ba3ba252c3f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 03:02:06 -0500 Subject: [PATCH 01/17] reorganize camera and controller initialization, allow setting them, WIP --- fastplotlib/layouts/_defaults.py | 43 -------- fastplotlib/layouts/_gridplot.py | 160 ++++++++++++++++++++---------- fastplotlib/layouts/_plot_area.py | 86 ++++++++++------ fastplotlib/layouts/_subplot.py | 57 +++++------ fastplotlib/layouts/_utils.py | 58 +++++++++++ 5 files changed, 246 insertions(+), 158 deletions(-) diff --git a/fastplotlib/layouts/_defaults.py b/fastplotlib/layouts/_defaults.py index 9a223855f..b28b04f64 100644 --- a/fastplotlib/layouts/_defaults.py +++ b/fastplotlib/layouts/_defaults.py @@ -1,46 +1,3 @@ -import pygfx -from typing import * -camera_types = { - "2d": pygfx.OrthographicCamera, - "3d": pygfx.PerspectiveCamera, -} -controller_types = { - "2d": pygfx.PanZoomController, - "3d": pygfx.OrbitController, - pygfx.OrthographicCamera: pygfx.PanZoomController, - pygfx.PerspectiveCamera: pygfx.OrbitController, -} - -def create_camera( - camera_type: str, big_camera: bool = False -) -> Union[pygfx.OrthographicCamera, pygfx.PerspectiveCamera]: - camera_type = camera_type.split("-") - - # kinda messy but works for now - if len(camera_type) > 1: - if camera_type[1] == "big": - big_camera = True - - camera_type = camera_type[0] - else: - camera_type = camera_type[0] - - cls = camera_types[camera_type] - - if cls is pygfx.OrthographicCamera: - if big_camera: - return cls(1024, 1024, -8192, 8192) - else: - return cls(1024, 1024) - - else: - return cls() - - -def create_controller(controller_type: str): - controller_type = controller_type.split("-")[0] - - return controller_types[controller_type]() diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 473196f78..dbb4c29f2 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -1,4 +1,4 @@ -from itertools import product +from itertools import product, chain import numpy as np from typing import * from inspect import getfullargspec @@ -9,8 +9,7 @@ from wgpu.gui.auto import WgpuCanvas from ._frame import Frame -from ._utils import make_canvas_and_renderer -from ._defaults import create_controller +from ._utils import make_canvas_and_renderer, create_controller from ._subplot import Subplot from ._record_mixin import RecordMixin @@ -25,18 +24,17 @@ def to_array(a) -> np.ndarray: return np.array(a) -valid_cameras = ["2d", "2d-big", "3d", "3d-big"] - - class GridPlot(Frame, RecordMixin): def __init__( self, shape: Tuple[int, int], - cameras: Union[np.ndarray, str] = "2d", - controllers: Union[np.ndarray, str] = None, + cameras: Union[str, list, np.ndarray] = "2d", + controller_types: Union[str, list, np.ndarray] = None, + controller_ids: Union[str, list, np.ndarray] = None, canvas: Union[str, WgpuCanvas, pygfx.Texture] = None, renderer: pygfx.WgpuRenderer = None, size: Tuple[int, int] = (500, 300), + names: Union[list, np.ndarray] = None, **kwargs, ): """ @@ -44,24 +42,29 @@ def __init__( Parameters ---------- - shape: tuple of int + shape: (int, int) (n_rows, n_cols) - cameras: np.ndarray or str, optional + cameras: str, list, or np.ndarray, optional | One of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots | OR - | Array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot + | list/array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot + + controller_types: str, list or np.ndarray, optional + list or array that specifies the controller type for each subplot. - controllers: np.ndarray or str, optional + controller_ids: str, list or np.ndarray of int or str ids, optional | If `None` a unique controller is created for each subplot | If "sync" all the subplots use the same controller | If ``numpy.array``, its shape must be the same as ``grid_shape``. This allows custom assignment of controllers - | Example: - | unique controllers for a 2x2 gridplot: np.array([[0, 1], [2, 3]]) - | same controllers for first 2 plots and last 2 plots: np.array([[0, 0, 1], [2, 3, 3]]) + | Example with integers: + | sync first 2 plots, and sync last 2 plots: [[0, 0, 1], [2, 3, 3]] + | Example with str subplot names: + | list of lists of subplot names, each sublist is synced: [[subplot_a, subplot_b], [subplot_f, subplot_c]] + | this syncs subplot_a and subplot_b together; syncs subplot_e and subplot_c together canvas: WgpuCanvas, optional Canvas for drawing @@ -69,62 +72,117 @@ def __init__( renderer: pygfx.Renderer, optional pygfx renderer instance - size: (int, int) + size: (int, int), optional starting size of canvas, default (500, 300) + names: list or array of str, optional + subplot names """ self.shape = shape + if names is not None: + self.names = to_array(names).reshape(self.shape) + else: + self.names = None + + if self.names.shape != self.shape: + raise ValueError("must provide same number of subplot `names` as specified by gridplot shape") + canvas, renderer = make_canvas_and_renderer(canvas, renderer) if isinstance(cameras, str): - if cameras not in valid_cameras: - raise ValueError( - f"If passing a str, `cameras` must be one of: {valid_cameras}" - ) # create the array representing the views for each subplot in the grid cameras = np.array([cameras] * self.shape[0] * self.shape[1]).reshape( self.shape ) - if isinstance(controllers, str): - if controllers == "sync": - controllers = np.zeros( - self.shape[0] * self.shape[1], dtype=int - ).reshape(self.shape) + # list -> array if necessary + cameras = to_array(cameras).reshape(self.shape) - if controllers is None: - controllers = np.arange(self.shape[0] * self.shape[1]).reshape(self.shape) + if cameras.shape != self.shape: + raise ValueError("Number of cameras does not match the number of subplots") - controllers = to_array(controllers) + if controller_ids is None: + # individual controller for each subplot + controller_ids = np.arange(self.shape[0] * self.shape[1]).reshape(self.shape) - if controllers.shape != self.shape: - raise ValueError + elif isinstance(controller_ids, str): + if controller_ids == "sync": + controller_ids = np.zeros(self.shape, dtype=int) - cameras = to_array(cameras) + # list controller_ids + elif isinstance(controller_ids, list): + flat = list(chain(*controller_ids)) - self._controllers = np.empty(shape=cameras.shape, dtype=object) + # list of str of subplot names, convert this to integer ids + if all([isinstance(item, str) for item in flat]): + if self.names is None: + raise ValueError("must specify subplot `names` to use list of str for `controller_ids`") - if cameras.shape != self.shape: - raise ValueError + # make sure each controller_id str is a subplot name + if not all([n in self.names for n in flat]): + raise KeyError( + f"all `controller_ids` strings must be one of the subplot names" + ) + + if len(flat) > len(set(flat)): + raise ValueError( + "id strings must not appear twice in `controller_ids`" + ) + + # initialize controller_ids array + ids_init = np.arange(self.shape[0] * self.shape[1]).reshape(self.shape) + + # set id based on subplot position for each synced sublist + for i, sublist in enumerate(controller_ids): + for name in sublist: + ids_init[self.names == name] = -(i + 1) # use negative numbers because why not - # create controllers if the arguments were integers - if np.issubdtype(controllers.dtype, np.integer): - if not np.all( - np.sort(np.unique(controllers)) - == np.arange(np.unique(controllers).size) - ): - raise ValueError("controllers must be consecutive integers") + controller_ids = ids_init + + # integer ids + elif all([isinstance(item, (int, np.integer)) for item in flat]): + controller_ids = to_array(controller_ids).reshape(self.shape) + + else: + raise TypeError( + f"list argument to `controller_ids` must be a list of `str` or `int`, " + f"you have passed: {controller_ids}" + ) + + controller_ids = to_array(controller_ids).reshape(self.shape) + + if controller_ids.shape != self.shape: + raise ValueError("Number of controller_ids does not match the number of subplots") + + if controller_types is None: + # create_controller will auto-determine controller for each subplot based on defaults + controller_types = np.array([None] * self.shape[0] * self.shape[1]).reshape(self.shape) + + # the controllers in this gridplot instance + self._controllers = np.empty(shape=cameras.shape, dtype=object) + + if np.issubdtype(controller_ids.dtype, np.integer): + # create controllers if the arguments were integers + # if not np.all( + # np.sort(np.unique(controller_ids)) + # == np.arange(np.unique(controller_ids).size) + # ): + # raise ValueError("numeric controllers_ids must be consecutive integers") for controller in np.unique(controllers): + # get the camera index for this controller cam = np.unique(cameras[controllers == controller]) if cam.size > 1: + # make sure that this controller is only for either a 2d or 3d camera raise ValueError( - f"Controller id: {controller} has been assigned to multiple different camera types" + f"Controller id: {controller} has been assigned to multiple different camera types: {cam}" ) - self._controllers[controllers == controller] = create_controller(cam[0]) + self._controllers[controllers == controller] = create_controller( + controller_type=controller, camera=cam[0] + ) # else assume it's a single pygfx.Controller instance or a list of controllers else: if isinstance(controllers, pygfx.Controller): @@ -140,13 +198,6 @@ def __init__( if renderer is None: renderer = pygfx.renderers.WgpuRenderer(canvas) - if "names" in kwargs.keys(): - self.names = to_array(kwargs["names"]) - if self.names.shape != self.shape: - raise ValueError - else: - self.names = None - self._canvas = canvas self._renderer = renderer @@ -305,4 +356,11 @@ def __next__(self) -> Subplot: return self._subplots[pos] def __repr__(self): - return f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}\n" + newline = "\n\t" + + return ( + f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}\n" + f" Subplots:\n" + f"\t{newline.join(subplot.__repr__() for subplot in self)}" + f"\n" + ) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index dec5d891e..e45421bed 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -6,18 +6,10 @@ import numpy as np import pygfx -from pygfx import ( - Scene, - OrthographicCamera, - PerspectiveCamera, - PanZoomController, - OrbitController, - Viewport, - WgpuRenderer, -) from pylinalg import vec_transform, vec_unproject from wgpu.gui.auto import WgpuCanvas +from ._utils import create_camera, create_controller from ..graphics._base import Graphic from ..graphics.selectors._base_selector import BaseSelector @@ -33,11 +25,11 @@ def __init__( self, parent, position: Any, - camera: Union[OrthographicCamera, PerspectiveCamera], - controller: Union[PanZoomController, OrbitController], - scene: Scene, + camera: Union[pygfx.Camera], + controller: Union[pygfx.Controller], + scene: pygfx.Scene, canvas: WgpuCanvas, - renderer: WgpuRenderer, + renderer: pygfx.WgpuRenderer, name: str = None, ): """ @@ -53,25 +45,23 @@ def __init__( typical use will be for ``subplots`` in a ``gridplot``, position would correspond to the ``[row, column]`` location of the ``subplot`` in its ``gridplot`` - camera: pygfx OrthographicCamera or pygfx PerspectiveCamera - ``OrthographicCamera`` type is used to visualize 2D content and ``PerspectiveCamera`` type is used to view - 3D content, used to view the scene + camera: pygfx.Camera + ``pygfx.OrthographicCamera`` is used for 2D scenes ``pygfx.PerspectiveCamera`` is used to view 3D scenes - controller: pygfx PanZoomController or pygfx OrbitController - ``PanZoomController`` type is used for 2D pan-zoom camera control and ``OrbitController`` type is used for - rotating the camera around a center position, used to control the camera + controller: pygfx.Controller + One of the pygfx controllers, panzoom, fly, orbit, or trackball - scene: pygfx Scene + scene: pygfx.Scene represents the root of a scene graph, will be viewed by the given ``camera`` canvas: WgpuCanvas provides surface on which a scene will be rendered - renderer: WgpuRenderer - object used to render scenes using wgpu + renderer: pygfx.WgpuRenderer + renders the scene onto the canvas name: str, optional - name of ``subplot`` or ``plot`` subclass being instantiated + name this ``subplot`` or ``plot`` """ @@ -82,14 +72,13 @@ def __init__( self._canvas = canvas self._renderer = renderer if parent is None: - self._viewport: Viewport = Viewport(renderer) + self._viewport: pygfx.Viewport = pygfx.Viewport(renderer) else: - self._viewport = Viewport(parent.renderer) + self._viewport = pygfx.Viewport(parent.renderer) self._camera = camera self._controller = controller - self.controller.add_camera(self.camera) self.controller.register_events( self.viewport, ) @@ -126,7 +115,7 @@ def position(self) -> Union[Tuple[int, int], Any]: return self._position @property - def scene(self) -> Scene: + def scene(self) -> pygfx.Scene: """The Scene where Graphics lie in this plot area""" return self._scene @@ -136,26 +125,59 @@ def canvas(self) -> WgpuCanvas: return self._canvas @property - def renderer(self) -> WgpuRenderer: + def renderer(self) -> pygfx.WgpuRenderer: """Renderer associated to the plot area""" return self._renderer @property - def viewport(self) -> Viewport: + def viewport(self) -> pygfx.Viewport: """The rectangular area of the renderer associated to this plot area""" return self._viewport @property - def camera(self) -> Union[OrthographicCamera, PerspectiveCamera]: + def camera(self) -> pygfx.Camera: """camera used to view the scene""" return self._camera + @camera.setter + def camera(self, new_camera: Union[str, pygfx.Camera]): + new_camera = create_camera(new_camera) + + # remove current camera from controller + self.controller.remove_camera(self._camera) + # add new camera to controller + self.controller.add_camera(new_camera) + + self._camera = new_camera + # in the future we can think about how to allow changing the controller @property - def controller(self) -> Union[PanZoomController, OrbitController]: - """controller used to control camera""" + def controller(self) -> pygfx.Controller: + """controller used to control the camera""" return self._controller + @controller.setter + def controller(self, new_controller: Union[str, pygfx.Controller]): + new_controller = create_controller(new_controller, self._camera) + + cameras_list = list() + + # remove all the cameras associated to this controller + for camera in self._controller.cameras: + self._controller.remove_camera(camera) + cameras_list.append(camera) + + del self._controller + + # add the associated cameras to the new controller + for camera in cameras_list: + if camera is self._camera: + # skip, already added in create_controller + continue + new_controller.add_camera(camera) + + self._controller = new_controller + @property def graphics(self) -> Tuple[Graphic, ...]: """Graphics in the plot area. Always returns a proxy to the Graphic instances.""" diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index c178c0fca..cce2d083a 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -2,22 +2,13 @@ import numpy as np -from pygfx import ( - Scene, - OrthographicCamera, - PanZoomController, - OrbitController, - AxesHelper, - GridHelper, - WgpuRenderer, - Texture, -) +import pygfx + from wgpu.gui.auto import WgpuCanvas from ..graphics import TextGraphic -from ._utils import make_canvas_and_renderer +from ._utils import make_canvas_and_renderer, create_camera, create_controller from ._plot_area import PlotArea -from ._defaults import create_camera, create_controller from .graphic_methods_mixin import GraphicMethodsMixin @@ -27,12 +18,11 @@ def __init__( parent: Any = None, position: Tuple[int, int] = None, parent_dims: Tuple[int, int] = None, - camera: str = "2d", - controller: Union[PanZoomController, OrbitController] = None, - canvas: Union[str, WgpuCanvas, Texture] = None, - renderer: WgpuRenderer = None, + camera: Union[str, pygfx.Camera] = "2d", + controller: Union[str, pygfx.Controller] = None, + canvas: Union[str, WgpuCanvas, pygfx.Texture] = None, + renderer: pygfx.WgpuRenderer = None, name: str = None, - **kwargs, ): """ General plot object that composes a ``Gridplot``. Each ``Gridplot`` instance will have [n rows, n columns] @@ -43,21 +33,22 @@ def __init__( Parameters ---------- - position: int tuple, optional + position: (int, int), optional corresponds to the [row, column] position of the subplot within a ``Gridplot`` - parent_dims: int tuple, optional + parent_dims: (int, int), optional dimensions of the parent ``GridPlot`` - camera: str, default '2d' + camera: str or pygfx.Camera, default '2d' indicates the kind of pygfx camera that will be instantiated, '2d' uses pygfx ``OrthographicCamera`` and '3d' uses pygfx ``PerspectiveCamera`` - controller: PanZoomController or OrbitOrthoController, optional - ``PanZoomController`` type is used for 2D pan-zoom camera control and ``OrbitController`` type is used for - rotating the camera around a center position, used to control the camera + controller: str or pygfx.Controller, optional + | if ``None``, uses a PanZoomController for "2d" camera or FlyController for "3d" camera. + | if ``str``, must be one of: `"panzoom", "fly", "trackball", or "orbit"`. + | also accepts a pygfx.Controller instance - canvas: WgpuCanvas, Texture, or one of "jupyter", "glfw", "qt", optional + canvas: one of "jupyter", "glfw", "qt", WgpuCanvas, or pygfx.Texture, optional Provides surface on which a scene will be rendered. Can optionally provide a WgpuCanvas instance or a str to force the PlotArea to use a specific canvas from one of the following options: "jupyter", "glfw", "qt". Can also provide a pygfx Texture to render to. @@ -82,25 +73,27 @@ def __init__( self.nrows, self.ncols = parent_dims + camera = create_camera(camera) + if controller is None: - controller = create_controller(camera) + controller = create_controller(controller_type=controller, camera=camera) self._docks = dict() self.spacing = 2 - self._axes: AxesHelper = AxesHelper(size=100) + self._axes: pygfx.AxesHelper = pygfx.AxesHelper(size=100) for arrow in self._axes.children: self._axes.remove(arrow) - self._grid: GridHelper = GridHelper(size=100, thickness=1) + self._grid: pygfx.GridHelper = pygfx.GridHelper(size=100, thickness=1) super(Subplot, self).__init__( parent=parent, position=position, - camera=create_camera(camera), + camera=camera, controller=controller, - scene=Scene(), + scene=pygfx.Scene(), canvas=canvas, renderer=renderer, name=name, @@ -221,9 +214,9 @@ def __init__( super(Dock, self).__init__( parent=parent, position=position, - camera=OrthographicCamera(), - controller=PanZoomController(), - scene=Scene(), + camera=pygfx.OrthographicCamera(), + controller=pygfx.PanZoomController(), + scene=pygfx.Scene(), canvas=parent.canvas, renderer=parent.renderer, ) diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index e8201d073..748e7ef44 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -1,5 +1,6 @@ from typing import * +import pygfx from pygfx import WgpuRenderer, Texture # default auto-determined canvas @@ -88,3 +89,60 @@ def make_canvas_and_renderer( renderer = WgpuRenderer(canvas) return canvas, renderer + + +camera_types = { + "2d": pygfx.OrthographicCamera, + "3d": pygfx.PerspectiveCamera, +} + + +def create_camera( + camera_type: Union[pygfx.Camera, str], +) -> Union[pygfx.OrthographicCamera, pygfx.PerspectiveCamera]: + if isinstance(camera_type, (pygfx.OrthographicCamera, pygfx.PerspectiveCamera)): + return camera_type + + if camera_type not in camera_types.keys(): + raise KeyError( + f"camera must be a valid pygfx.Camera or one of: " + f"{list(camera_types.keys())}, you have passed: {camera_type}" + ) + + return camera_types[camera_type]() + + +controller_types = { + "fly": pygfx.FlyController, + "panzoom": pygfx.PanZoomController, + "trackball": pygfx.TrackballController, + "orbit": pygfx.OrbitController, +} + + +def create_controller( + controller_type: Union[pygfx.Controller, None, str], + camera: Union[pygfx.Camera], +) -> pygfx.Controller: + """ + Creates the controllers and adds the camera to it. + """ + if isinstance(controller_type, pygfx.Controller): + controller_type.add_camera(camera) + return controller_type + + if controller_type is None: + # default controllers + if camera == "2d" or isinstance(camera, pygfx.OrthographicCamera): + return pygfx.PanZoomController(camera) + + elif camera == "3d" or isinstance(camera, pygfx.PerspectiveCamera): + return pygfx.FlyController(camera) + + if controller_type not in controller_types.keys(): + raise KeyError( + f"controller must be a valid pygfx.Controller or one of: " + f"{list(controller_types.keys())}, you have passed: {controller_type}" + ) + + return controller_types[controller_type]() From c0c37ecd6db07b4bb7fb4ea40cab8ef22f692de6 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 03:38:30 -0500 Subject: [PATCH 02/17] controllers named sync done, not tested yet --- fastplotlib/layouts/_gridplot.py | 80 ++++++++++++++++++-------------- fastplotlib/layouts/_utils.py | 3 +- 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index dbb4c29f2..e104d2626 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -9,7 +9,8 @@ from wgpu.gui.auto import WgpuCanvas from ._frame import Frame -from ._utils import make_canvas_and_renderer, create_controller +from ._utils import make_canvas_and_renderer, create_controller, create_camera +from ._utils import controller_types as valid_controller_types from ._subplot import Subplot from ._record_mixin import RecordMixin @@ -103,6 +104,11 @@ def __init__( if cameras.shape != self.shape: raise ValueError("Number of cameras does not match the number of subplots") + # create the cameras + self._cameras = np.empty(self.shape, dtype=object) + for i, j in product(range(self.shape[0]), range(self.shape[1])): + self._cameras[i, j] = create_camera(camera_type=cameras[i, j]) + if controller_ids is None: # individual controller for each subplot controller_ids = np.arange(self.shape[0] * self.shape[1]).reshape(self.shape) @@ -112,7 +118,7 @@ def __init__( controller_ids = np.zeros(self.shape, dtype=int) # list controller_ids - elif isinstance(controller_ids, list): + elif isinstance(controller_ids, (list, np.ndarray)): flat = list(chain(*controller_ids)) # list of str of subplot names, convert this to integer ids @@ -151,46 +157,50 @@ def __init__( f"you have passed: {controller_ids}" ) - controller_ids = to_array(controller_ids).reshape(self.shape) - if controller_ids.shape != self.shape: raise ValueError("Number of controller_ids does not match the number of subplots") if controller_types is None: - # create_controller will auto-determine controller for each subplot based on defaults + # `create_controller()` will auto-determine controller for each subplot based on defaults controller_types = np.array([None] * self.shape[0] * self.shape[1]).reshape(self.shape) - # the controllers in this gridplot instance - self._controllers = np.empty(shape=cameras.shape, dtype=object) - - if np.issubdtype(controller_ids.dtype, np.integer): - # create controllers if the arguments were integers - # if not np.all( - # np.sort(np.unique(controller_ids)) - # == np.arange(np.unique(controller_ids).size) - # ): - # raise ValueError("numeric controllers_ids must be consecutive integers") - - for controller in np.unique(controllers): - # get the camera index for this controller - cam = np.unique(cameras[controllers == controller]) - if cam.size > 1: - # make sure that this controller is only for either a 2d or 3d camera - raise ValueError( - f"Controller id: {controller} has been assigned to multiple different camera types: {cam}" - ) + # validate controller types + flat = list(chain(*controller_types)) + # str controller_type or pygfx instances + valid_str = list(valid_controller_types.keys()) + valid_instances = tuple(valid_controller_types.values()) + + # make sure each controller type is valid + for controller_type in flat: + if (controller_type not in valid_str) and (not isinstance(controller_type, valid_instances)): + raise ValueError( + f"You have passed an invalid controller type, valid controller_types arguments are:\n" + f"{valid_str} or instances of {[c.__name__ for c in valid_instances]}" + ) + + controller_types = to_array(controller_types).reshape(self.shape) - self._controllers[controllers == controller] = create_controller( - controller_type=controller, camera=cam[0] + # make the real controllers for each subplot + self._controllers = np.empty(shape=self.shape, dtype=object) + for cid in np.unique(controller_ids): + _cont_type = controller_types[controller_ids == cid] + if np.unique(_cont_type).size > 1: + raise ValueError( + "Multiple controller types have been assigned to the same controller id. " + "All controllers with the same id must use the same type of controller." ) - # else assume it's a single pygfx.Controller instance or a list of controllers - else: - if isinstance(controllers, pygfx.Controller): - self._controllers = np.array( - [controllers] * shape[0] * shape[1] - ).reshape(shape) - else: - self._controllers = np.array(controllers).reshape(shape) + + # get all the cameras that use this controller + _cams = self._cameras[controller_ids == cid].ravel() + + _controller = create_controller(controller_type=_cont_type, camera=_cams[0]) + + self._controllers[controller_ids == cid] = _controller + + # add the other cameras that go with this controller + if _cams.size > 1: + for _cam in _cams[1:]: + _controller.add_camera(_cam) if canvas is None: canvas = WgpuCanvas() @@ -209,7 +219,7 @@ def __init__( for i, j in self._get_iterator(): position = (i, j) - camera = cameras[i, j] + camera = self._cameras[i, j] controller = self._controllers[i, j] if self.names is not None: diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 748e7ef44..640a92c7b 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -128,7 +128,8 @@ def create_controller( Creates the controllers and adds the camera to it. """ if isinstance(controller_type, pygfx.Controller): - controller_type.add_camera(camera) + if camera not in controller_type.cameras: + controller_type.add_camera(camera) return controller_type if controller_type is None: From 9e9af57e7d1cb7258b11bf59c68c56b654c77b1d Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 04:56:31 -0500 Subject: [PATCH 03/17] new controllers work, ids also works, changing controllers works --- fastplotlib/layouts/_gridplot.py | 37 ++++++++++++++++++++----------- fastplotlib/layouts/_plot.py | 12 +++++----- fastplotlib/layouts/_plot_area.py | 19 +++++++++++----- fastplotlib/layouts/_utils.py | 5 ++--- 4 files changed, 46 insertions(+), 27 deletions(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index e104d2626..65eda7f34 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -83,13 +83,13 @@ def __init__( self.shape = shape if names is not None: + if len(list(chain(*names))) != self.shape[0] * self.shape[1]: + raise ValueError("must provide same number of subplot `names` as specified by gridplot shape") + self.names = to_array(names).reshape(self.shape) else: self.names = None - if self.names.shape != self.shape: - raise ValueError("must provide same number of subplot `names` as specified by gridplot shape") - canvas, renderer = make_canvas_and_renderer(canvas, renderer) if isinstance(cameras, str): @@ -162,16 +162,19 @@ def __init__( if controller_types is None: # `create_controller()` will auto-determine controller for each subplot based on defaults - controller_types = np.array([None] * self.shape[0] * self.shape[1]).reshape(self.shape) + controller_types = np.array(["default"] * self.shape[0] * self.shape[1]).reshape(self.shape) # validate controller types flat = list(chain(*controller_types)) # str controller_type or pygfx instances - valid_str = list(valid_controller_types.keys()) + valid_str = list(valid_controller_types.keys()) + ["default"] valid_instances = tuple(valid_controller_types.values()) # make sure each controller type is valid for controller_type in flat: + if controller_type is None: + continue + if (controller_type not in valid_str) and (not isinstance(controller_type, valid_instances)): raise ValueError( f"You have passed an invalid controller type, valid controller_types arguments are:\n" @@ -183,24 +186,29 @@ def __init__( # make the real controllers for each subplot self._controllers = np.empty(shape=self.shape, dtype=object) for cid in np.unique(controller_ids): - _cont_type = controller_types[controller_ids == cid] - if np.unique(_cont_type).size > 1: + cont_type = controller_types[controller_ids == cid] + if np.unique(cont_type).size > 1: raise ValueError( "Multiple controller types have been assigned to the same controller id. " "All controllers with the same id must use the same type of controller." ) + cont_type = cont_type[0] + # get all the cameras that use this controller - _cams = self._cameras[controller_ids == cid].ravel() + cams = self._cameras[controller_ids == cid].ravel() - _controller = create_controller(controller_type=_cont_type, camera=_cams[0]) + if cont_type == "default": + # hacky fix for now because of hwo `create_controller()` works + cont_type = None + _controller = create_controller(controller_type=cont_type, camera=cams[0]) self._controllers[controller_ids == cid] = _controller # add the other cameras that go with this controller - if _cams.size > 1: - for _cam in _cams[1:]: - _controller.add_camera(_cam) + if cams.size > 1: + for cam in cams[1:]: + _controller.add_camera(cam) if canvas is None: canvas = WgpuCanvas() @@ -365,12 +373,15 @@ def __next__(self) -> Subplot: pos = self._current_iter.__next__() return self._subplots[pos] + def __str__(self): + return f"{self.__class__.__name__} @ {hex(id(self))}" + def __repr__(self): newline = "\n\t" return ( f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}\n" f" Subplots:\n" - f"\t{newline.join(subplot.__repr__() for subplot in self)}" + f"\t{newline.join(subplot.__str__() for subplot in self)}" f"\n" ) diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 5aa04bb76..3c205e0ec 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -13,8 +13,8 @@ def __init__( self, canvas: WgpuCanvas = None, renderer: pygfx.WgpuRenderer = None, - camera: str = "2d", - controller: Union[pygfx.PanZoomController, pygfx.OrbitController] = None, + camera: Union[str, pygfx.Camera] = "2d", + controller: Union[str, pygfx.Controller] = None, size: Tuple[int, int] = (500, 300), **kwargs, ): @@ -29,12 +29,14 @@ def __init__( renderer: pygfx.Renderer, optional pygfx renderer instance - camera:str, optional + camera: str or pygfx.Camera, optional | One of ``"2d"`` or ``"3d"`` indicating 2D or 3D camera - controller: None, PanZoomController or OrbitOrthoController, optional + controller: str or pygfx.Controller, optional Usually ``None``, you can pass an existing controller from another - ``Plot`` or ``Subplot`` within a ``GridPlot`` to synchronize them. + ``Plot`` or ``Subplot`` to synchronize them. + + You can also pass str arguments of valid controller names, see Subplot docstring for valid names size: (int, int) starting size of canvas, default (500, 300) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index e45421bed..af758234e 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -167,15 +167,22 @@ def controller(self, new_controller: Union[str, pygfx.Controller]): self._controller.remove_camera(camera) cameras_list.append(camera) - del self._controller - # add the associated cameras to the new controller for camera in cameras_list: - if camera is self._camera: - # skip, already added in create_controller - continue new_controller.add_camera(camera) + new_controller.register_events( + self.viewport + ) + + # monkeypatch until we figure out a better way + if self.parent is not None: + if self.parent.__class__.__name__ == "GridPlot": + for subplot in self.parent: + if subplot.camera in cameras_list: + new_controller.register_events(subplot.viewport) + subplot._controller = new_controller + self._controller = new_controller @property @@ -685,7 +692,7 @@ def __repr__(self): return ( f"{self}\n" - f" parent: {self.parent}\n" + f" parent: {self.parent.__str__()}\n" f" Graphics:\n" f"\t{newline.join(graphic.__repr__() for graphic in self.graphics)}" f"\n" diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 640a92c7b..115eac9f7 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -128,8 +128,7 @@ def create_controller( Creates the controllers and adds the camera to it. """ if isinstance(controller_type, pygfx.Controller): - if camera not in controller_type.cameras: - controller_type.add_camera(camera) + controller_type.add_camera(camera) return controller_type if controller_type is None: @@ -146,4 +145,4 @@ def create_controller( f"{list(controller_types.keys())}, you have passed: {controller_type}" ) - return controller_types[controller_type]() + return controller_types[controller_type](camera) From b72fcd7c1e09cf9d70a87caf44f91a0c0c711ff7 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 05:08:47 -0500 Subject: [PATCH 04/17] bugfix --- fastplotlib/layouts/_subplot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index cce2d083a..99d804aea 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -75,8 +75,7 @@ def __init__( camera = create_camera(camera) - if controller is None: - controller = create_controller(controller_type=controller, camera=camera) + controller = create_controller(controller_type=controller, camera=camera) self._docks = dict() From 7e233375b9db962101db3bad7636fb037604f477 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 05:36:12 -0500 Subject: [PATCH 05/17] update nb examples --- examples/notebooks/gridplot.ipynb | 150 ++++------------------- examples/notebooks/gridplot_simple.ipynb | 142 ++++++--------------- examples/notebooks/image_widget.ipynb | 14 ++- examples/notebooks/scatter.ipynb | 14 +-- examples/notebooks/simple.ipynb | 44 ++++++- 5 files changed, 120 insertions(+), 244 deletions(-) diff --git a/examples/notebooks/gridplot.ipynb b/examples/notebooks/gridplot.ipynb index cfcae3705..f1ceb2180 100644 --- a/examples/notebooks/gridplot.ipynb +++ b/examples/notebooks/gridplot.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "a635b3b3-33fa-48f0-b1cc-bf83b1e883ab", "metadata": {}, "outputs": [], @@ -21,52 +21,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "8de931e2-bdb3-44a3-9538-e0b3965779af", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "62f67ebf3b00494c826d92fc5b8bbf76", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b6262056cff84619aea08fc48d00eb79", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# grid with 2 rows and 3 columns\n", "grid_shape = (2, 3)\n", @@ -74,9 +32,15 @@ "# pan-zoom controllers for each view\n", "# views are synced if they have the \n", "# same controller ID\n", - "controllers = [\n", - " [0, 3, 1], # id each controller with an integer\n", - " [2, 2, 3]\n", + "controller_ids = [\n", + " [0, -3, 1], # id each controller with an integer\n", + " [2, 2, -3]\n", + "]\n", + "\n", + "# another way to set controller_ids\n", + "controller_ids = [\n", + " [\"subplot0\", \"subplot4\"],\n", + " [\"subplot1\", \"subplot2\", \"subplot5\"],\n", "]\n", "\n", "\n", @@ -89,7 +53,7 @@ "# Create the grid plot\n", "grid_plot = GridPlot(\n", " shape=grid_shape,\n", - " controllers=controllers,\n", + " controller_ids=controller_ids,\n", " names=names,\n", ")\n", "\n", @@ -123,24 +87,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "2a6f7eb5-776e-42a6-b6c2-c26009a26795", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "subplot0: Subplot @ 0x7fd496b32830\n", - " parent: None\n", - " Graphics:\n", - "\t'rand-image': ImageGraphic @ 0x7fd496b327d0" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# can access subplot by name\n", "grid_plot[\"subplot0\"]" @@ -148,24 +98,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "45e83bca-5a44-48ce-874f-9ae9ca444233", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "subplot0: Subplot @ 0x7fd496b32830\n", - " parent: None\n", - " Graphics:\n", - "\t'rand-image': ImageGraphic @ 0x7fd496b327d0" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# can access subplot by index\n", "grid_plot[0, 0]" @@ -182,21 +118,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "c8cf9bfd-e0cc-4173-b64e-a9f2c87bb2c6", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'rand-image': ImageGraphic @ 0x7fd496b327d0" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# can access graphic directly via name\n", "grid_plot[\"subplot0\"][\"rand-image\"]" @@ -204,13 +129,13 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "1cfd1d45-8a60-4fc1-b873-46caa966fe6f", "metadata": {}, "outputs": [], "source": [ - "grid_plot[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", - "grid_plot[\"subplot0\"][\"rand-image\"].vmax = 0.8" + "grid_plot[\"subplot0\"][\"rand-image\"].cmap.vmin = 0.6\n", + "grid_plot[\"subplot0\"][\"rand-image\"].cmap.vmax = 0.8" ] }, { @@ -223,40 +148,19 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "2fafe992-4783-40f2-b044-26a2835dd50a", "metadata": {}, "outputs": [], "source": [ - "grid_plot[1, 0][\"rand-image\"].vim = 0.1\n", - "grid_plot[1, 0][\"rand-image\"].vmax = 0.3" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "a025b76c-77f8-4aeb-ac33-5bb6d0bb5a9a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'image'" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grid_plot[1, 0][\"rand-image\"].type" + "grid_plot[1, 0][\"rand-image\"].cmap.vim = 0.1\n", + "grid_plot[1, 0][\"rand-image\"].cmap.vmax = 0.3" ] }, { "cell_type": "code", "execution_count": null, - "id": "62584f91-a8ed-4317-98ee-28ad6d1800d6", + "id": "a61e34a5-ee1b-4abb-8718-ec4715ffaa52", "metadata": {}, "outputs": [], "source": [] @@ -278,7 +182,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/notebooks/gridplot_simple.ipynb b/examples/notebooks/gridplot_simple.ipynb index 8b50b2701..74807f55a 100644 --- a/examples/notebooks/gridplot_simple.ipynb +++ b/examples/notebooks/gridplot_simple.ipynb @@ -10,9 +10,13 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "5171a06e-1bdc-4908-9726-3c1fd45dbb9d", "metadata": { + "ExecuteTime": { + "end_time": "2023-11-26T04:01:19.120171Z", + "start_time": "2023-11-26T04:01:18.618087Z" + }, "tags": [] }, "outputs": [], @@ -23,38 +27,19 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "86a2488f-ae1c-4b98-a7c0-18eae8013af1", "metadata": { + "ExecuteTime": { + "end_time": "2023-11-26T04:01:19.467264Z", + "start_time": "2023-11-26T04:01:19.121813Z" + }, "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f9067cd724094b8c8dfecf60208acbfa", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/_features/_base.py:34: UserWarning: converting float64 array to float32\n", - " warn(f\"converting {array.dtype} array to float32\")\n" - ] - } - ], + "outputs": [], "source": [ "# GridPlot of shape 2 x 3 with all controllers synced\n", - "grid_plot = GridPlot(shape=(2, 3), controllers=\"sync\")\n", + "grid_plot = GridPlot(shape=(2, 3), controller_ids=\"sync\")\n", "\n", "# Make a random image graphic for each subplot\n", "for subplot in grid_plot:\n", @@ -88,27 +73,22 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, + "id": "52163bc7-2c77-4699-b7b0-e455a0ed7584", + "metadata": {}, + "outputs": [], + "source": [ + "grid_plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, "id": "17c6bc4a-5340-49f1-8597-f54528cfe915", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "unnamed: Subplot @ 0x7f15df4f5c50\n", - " parent: fastplotlib.GridPlot @ 0x7f15d3f27890\n", - "\n", - " Graphics:\n", - "\t'rand-img': ImageGraphic @ 0x7f15d3fb5390" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# positional indexing\n", "# row 0 and col 0\n", @@ -125,23 +105,12 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "34130f12-9ef6-43b0-b929-931de8b7da25", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "(,)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "grid_plot[0, 1].graphics" ] @@ -156,7 +125,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "ef8a29a6-b19c-4e6b-a2ba-fb4823c01451", "metadata": { "tags": [] @@ -176,7 +145,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "d6c2fa4b-c634-4dcf-8b61-f1986f7c4918", "metadata": { "tags": [] @@ -189,50 +158,24 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "2f6b549c-3165-496d-98aa-45b96c3de674", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "top-right-plot: Subplot @ 0x7f15d3f769d0\n", - " parent: fastplotlib.GridPlot @ 0x7f15d3f27890\n", - "\n", - " Graphics:\n", - "\t'rand-img': ImageGraphic @ 0x7f15b83f7250" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "grid_plot[\"top-right-plot\"]" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "be436e04-33a6-4597-8e6a-17e1e5225419", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "(0, 2)" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# view its position\n", "grid_plot[\"top-right-plot\"].position" @@ -240,23 +183,12 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "6699cda6-af86-4258-87f5-1832f989a564", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# these are really the same\n", "grid_plot[\"top-right-plot\"] is grid_plot[0, 2]" @@ -272,19 +204,19 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "545b627b-d794-459a-a75a-3fde44f0ea95", "metadata": { "tags": [] }, "outputs": [], "source": [ - "grid_plot[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" + "grid_plot[\"top-right-plot\"][\"rand-img\"].cmap.vmin = 0.5" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "36432d5b-b76c-4a2a-a32c-097faf5ab269", "metadata": { "tags": [] diff --git a/examples/notebooks/image_widget.ipynb b/examples/notebooks/image_widget.ipynb index 296126f5e..56d5c8a81 100644 --- a/examples/notebooks/image_widget.ipynb +++ b/examples/notebooks/image_widget.ipynb @@ -235,6 +235,16 @@ "iw_movie.gridplot[0, 0].auto_scale()# sidecar is optional" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d323f67-4717-4241-ad84-5091e6caf2cd", + "metadata": {}, + "outputs": [], + "source": [ + "iw_movie.close()" + ] + }, { "cell_type": "markdown", "id": "aca22179-1b1f-4c51-97bf-ce2d7044e451", @@ -296,7 +306,7 @@ }, { "cell_type": "markdown", - "id": "0721dc40-677e-431d-94c6-da59606199cb", + "id": "037d899e-9e7c-4d58-a021-5f5b71e1db90", "metadata": {}, "source": [ "pan-zoom controllers are all synced across subplots in a `ImageWidget`" @@ -473,7 +483,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/notebooks/scatter.ipynb b/examples/notebooks/scatter.ipynb index 948403f11..9d7ff099f 100644 --- a/examples/notebooks/scatter.ipynb +++ b/examples/notebooks/scatter.ipynb @@ -78,7 +78,7 @@ "# same controller ID\n", "# you can only sync controllers that use the same camera type\n", "# i.e. you cannot sync between 2d and 3d subplots\n", - "controllers = [\n", + "controller_ids = [\n", " [0, 1],\n", " [1, 0]\n", "]\n", @@ -87,7 +87,7 @@ "grid_plot = GridPlot(\n", " shape=shape,\n", " cameras=cameras,\n", - " controllers=controllers\n", + " controller_ids=controller_ids\n", ")\n", "\n", "for subplot in grid_plot:\n", @@ -163,15 +163,7 @@ { "cell_type": "code", "execution_count": null, - "id": "fb49930f-b795-4b41-bbc6-014a27c2f463", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7d8aac54-4f36-41d4-8e5b-8d8da2f0d17d", + "id": "a18e7a17-c2af-4674-a499-bf5f3b27c8ca", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/simple.ipynb index 681980d39..f5901080b 100644 --- a/examples/notebooks/simple.ipynb +++ b/examples/notebooks/simple.ipynb @@ -100,7 +100,7 @@ "image_graphic = plot.add_image(data=data, name=\"sample-image\")\n", "\n", "# show the plot\n", - "plot.show()" + "plot.show(sidecar=True)" ] }, { @@ -589,7 +589,7 @@ "\n", "plot_sync.add_animations(update_data_2)\n", "\n", - "plot_sync.show(sidecar=False)" + "plot_sync.show()" ] }, { @@ -704,7 +704,7 @@ "sinc_graphic = plot_l.add_line(data=sinc, thickness=5, colors = colors)\n", "\n", "# show the plot\n", - "plot_l.show(sidecar_kwargs={\"title\": \"lines\", \"layout\": {'width': '800px'}})" + "plot_l.show(sidecar=True, sidecar_kwargs={\"title\": \"lines\"})" ] }, { @@ -1020,6 +1020,14 @@ "plot_l3d.show()" ] }, + { + "cell_type": "markdown", + "id": "29f07af0-cdcb-47cc-bbb3-2fa4449fa084", + "metadata": {}, + "source": [ + "**Use WASD keys and the mouse to move around, just like in a game :D. Use the mouse weel to control the speed of movement.**" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1045,6 +1053,17 @@ "plot_test(\"lines-3d\", plot_l3d)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a6da884-8b40-4ebf-837f-929b3e9cf4c4", + "metadata": {}, + "outputs": [], + "source": [ + "# change the FOV of the persepctive camera\n", + "plot_l3d.camera.fov = 70" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1187,6 +1206,25 @@ "scatter_graphic.data[n_points:n_points * 2, 0] = np.linspace(-40, 0, n_points)" ] }, + { + "cell_type": "markdown", + "id": "5f3e206d-97af-4e07-9969-94f2fdb41004", + "metadata": {}, + "source": [ + "**Switch to a fly controller to move around the plot in 3D!**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c67944ca-52e7-4213-b820-6572cc3f76f0", + "metadata": {}, + "outputs": [], + "source": [ + "plot_s.camera = \"3d\"\n", + "plot_s.controller = \"fly\"" + ] + }, { "cell_type": "code", "execution_count": null, From 723354cc38364ae276b5b2e7dbde1e2776fa4479 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 05:50:10 -0500 Subject: [PATCH 06/17] update nbs --- examples/notebooks/lineplot.ipynb | 62 ++----------------- .../notebooks/scatter_sizes_animation.ipynb | 52 ++++++++++++---- examples/notebooks/scatter_sizes_grid.ipynb | 26 ++++---- 3 files changed, 56 insertions(+), 84 deletions(-) diff --git a/examples/notebooks/lineplot.ipynb b/examples/notebooks/lineplot.ipynb index e156a7150..667cae178 100644 --- a/examples/notebooks/lineplot.ipynb +++ b/examples/notebooks/lineplot.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "9c974494-712e-4981-bae2-a3ee176a6b20", "metadata": {}, "outputs": [], @@ -23,7 +23,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "c3d8f967-f60f-4f0b-b6ba-21b1251b4856", "metadata": {}, "outputs": [], @@ -41,60 +41,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "78cffe56-1147-4255-82c1-53cec6bc986a", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "7993e0a4358f4678a7343b78b3b0b24c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/kushalk/repos/fastplotlib/fastplotlib/layouts/_base.py:214: UserWarning: `center_scene()` not yet implemented for `PerspectiveCamera`\n", - " warn(\"`center_scene()` not yet implemented for `PerspectiveCamera`\")\n" - ] - }, - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "898a109f489741a5b4624be77bd27db0", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# grid with 2 rows and 2 columns\n", "shape = (2, 2)\n", @@ -104,7 +54,7 @@ "# same controller ID\n", "# in this example the first view has its own controller\n", "# and the last 3 views are synced\n", - "controllers = [\n", + "controller_ids = [\n", " [0, 1], # id each controller with an integer\n", " [1, 1]\n", "]\n", @@ -113,7 +63,7 @@ "grid_plot = GridPlot(\n", " shape=shape,\n", " cameras='3d', # 3D view for all subplots within the grid\n", - " controllers=controllers\n", + " controller_ids=controller_ids\n", ")\n", "\n", "for i, subplot in enumerate(grid_plot):\n", diff --git a/examples/notebooks/scatter_sizes_animation.ipynb b/examples/notebooks/scatter_sizes_animation.ipynb index 061f444d6..06a6b11a2 100644 --- a/examples/notebooks/scatter_sizes_animation.ipynb +++ b/examples/notebooks/scatter_sizes_animation.ipynb @@ -2,9 +2,39 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5d9f9913391a42af95d4d43d07c17b19", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9cd08c319b814934a09fd266a1b6322b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from time import time\n", "\n", @@ -21,8 +51,6 @@ " current_time = time()\n", " newPositions = points + np.sin(((current_time / 4) % 1)*np.pi)\n", " plot.graphics[0].data = newPositions\n", - " plot.camera.width = 4*np.max(newPositions[0,:])\n", - " plot.camera.height = 4*np.max(newPositions[1,:])\n", "\n", "def update_sizes():\n", " current_time = time()\n", @@ -30,12 +58,11 @@ " size_delta = sin_sample*size_delta_scales\n", " plot.graphics[0].sizes = min_sizes + size_delta\n", "\n", - "points = np.array([[0,0], \n", - " [1,1], \n", - " [2,2]])\n", "scatter = plot.add_scatter(points, colors=[\"red\", \"green\", \"blue\"], sizes=12)\n", "plot.add_animations(update_positions, update_sizes)\n", - "plot.show(autoscale=True)" + "\n", + "plot.camera.width = 12\n", + "plot.show(autoscale=False)" ] }, { @@ -48,7 +75,7 @@ ], "metadata": { "kernelspec": { - "display_name": "fastplotlib-dev", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -62,10 +89,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" - }, - "orig_nbformat": 4 + "version": "3.11.3" + } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/examples/notebooks/scatter_sizes_grid.ipynb b/examples/notebooks/scatter_sizes_grid.ipynb index ff64184f7..e152056c9 100644 --- a/examples/notebooks/scatter_sizes_grid.ipynb +++ b/examples/notebooks/scatter_sizes_grid.ipynb @@ -19,15 +19,6 @@ "# grid with 2 rows and 3 columns\n", "grid_shape = (2,1)\n", "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", - "# same controller ID\n", - "controllers = [\n", - " [0],\n", - " [0]\n", - "]\n", - "\n", - "\n", "# you can give string names for each subplot within the gridplot\n", "names = [\n", " [\"scalar_size\"],\n", @@ -37,7 +28,6 @@ "# Create the grid plot\n", "plot = fpl.GridPlot(\n", " shape=grid_shape,\n", - " controllers=controllers,\n", " names=names,\n", " size=(1000, 1000)\n", ")\n", @@ -59,11 +49,18 @@ "\n", "plot.show()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "fastplotlib-dev", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -77,10 +74,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" - }, - "orig_nbformat": 4 + "version": "3.11.3" + } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } From 5e384abe65489222228008454992dfbcee940833 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 05:52:26 -0500 Subject: [PATCH 07/17] controller changes for imagewidget --- fastplotlib/layouts/_gridplot.py | 1 - fastplotlib/layouts/_plot_area.py | 1 + fastplotlib/widgets/image.py | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 65eda7f34..a8f370875 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -36,7 +36,6 @@ def __init__( renderer: pygfx.WgpuRenderer = None, size: Tuple[int, int] = (500, 300), names: Union[list, np.ndarray] = None, - **kwargs, ): """ A grid of subplots. diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index af758234e..b0b4b137b 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -79,6 +79,7 @@ def __init__( self._camera = camera self._controller = controller + self.controller.add_camera(self._camera) self.controller.register_events( self.viewport, ) diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 522b0a962..56aa4aebc 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -550,7 +550,7 @@ def __init__( self._dims_max_bounds[_dim], array.shape[order.index(_dim)] ) - grid_plot_kwargs_default = {"controllers": "sync"} + grid_plot_kwargs_default = {"controller_ids": "sync"} if grid_plot_kwargs is None: grid_plot_kwargs = dict() @@ -885,6 +885,7 @@ def set_data( frame = self._process_frame_apply(frame, i) new_graphic = ImageGraphic(data=frame, name="image_widget_managed") subplot.insert_graphic(graphic=new_graphic) + subplot.docks["right"]["histogram_lut"].image_graphic = new_graphic if new_array.ndim > 2: # to set max of time slider, txy or tzxy From 92cae59282b32cc56cd8335222067b71b4e71963 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 05:54:04 -0500 Subject: [PATCH 08/17] fix type annotation --- fastplotlib/layouts/_plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 3c205e0ec..2937dc27b 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -11,7 +11,7 @@ class Plot(Subplot, Frame, RecordMixin): def __init__( self, - canvas: WgpuCanvas = None, + canvas: Union[str, WgpuCanvas] = None, renderer: pygfx.WgpuRenderer = None, camera: Union[str, pygfx.Camera] = "2d", controller: Union[str, pygfx.Controller] = None, From 4b4a31dd79a08c5dc7c8665a47f85891d775c270 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 05:54:18 -0500 Subject: [PATCH 09/17] update example --- examples/desktop/gridplot/gridplot_non_square.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/desktop/gridplot/gridplot_non_square.py b/examples/desktop/gridplot/gridplot_non_square.py index a41bcd9f4..fe43a3c04 100644 --- a/examples/desktop/gridplot/gridplot_non_square.py +++ b/examples/desktop/gridplot/gridplot_non_square.py @@ -10,7 +10,7 @@ import imageio.v3 as iio -plot = fpl.GridPlot(shape=(2, 2), controllers="sync") +plot = fpl.GridPlot(shape=(2, 2), controller_ids="sync") # to force a specific framework such as glfw: # plot = fpl.GridPlot(canvas="glfw") From 595474097cf4cb97c40e79d308384ea59f1970e8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 06:04:32 -0500 Subject: [PATCH 10/17] update example --- examples/desktop/scatter/scatter_size.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/examples/desktop/scatter/scatter_size.py b/examples/desktop/scatter/scatter_size.py index 5b6987b7c..2ad995584 100644 --- a/examples/desktop/scatter/scatter_size.py +++ b/examples/desktop/scatter/scatter_size.py @@ -9,16 +9,7 @@ import fastplotlib as fpl # grid with 2 rows and 3 columns -grid_shape = (2,1) - -# pan-zoom controllers for each view -# views are synced if they have the -# same controller ID -controllers = [ - [0], - [0] -] - +grid_shape = (2, 1) # you can give string names for each subplot within the gridplot names = [ @@ -29,7 +20,6 @@ # Create the grid plot plot = fpl.GridPlot( shape=grid_shape, - controllers=controllers, names=names, size=(1000, 1000) ) @@ -53,4 +43,4 @@ if __name__ == "__main__": print(__doc__) - fpl.run() \ No newline at end of file + fpl.run() From 7f6e5dca8271a3955d858d3428fb2c654f137af6 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 06:04:41 -0500 Subject: [PATCH 11/17] update quickstart nb --- docs/source/quickstart.ipynb | 46 ++++-------------------------------- 1 file changed, 4 insertions(+), 42 deletions(-) diff --git a/docs/source/quickstart.ipynb b/docs/source/quickstart.ipynb index 6a3afec33..0de4667bf 100644 --- a/docs/source/quickstart.ipynb +++ b/docs/source/quickstart.ipynb @@ -97,21 +97,7 @@ "id": "be5b408f-dd91-4e36-807a-8c22c8d7d216", "metadata": {}, "source": [ - "**In live notebooks or desktop applications, you can use the handle on the bottom right corner of the _canvas_ to resize it. You can also pan and zoom using your mouse!**\n", - "\n", - "By default the origin is on the bottom left, you can click the flip button to flip the y-axis, or use `plot.camera.world.scale_y *= -1`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9b4e977e-2a7d-4e2b-aee4-cfc36767b3c6", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot.camera.world.scale_y *= -1" + "**In live notebooks or desktop applications, you can use the handle on the bottom right corner of the _canvas_ to resize it. You can also pan and zoom using your mouse!**" ] }, { @@ -510,18 +496,6 @@ "plot_rgb.show()" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "7e4b5a30-4293-4ae3-87dc-06a1355bb2c7", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot_rgb.camera.world.scale_y *= -1" - ] - }, { "cell_type": "code", "execution_count": null, @@ -1361,7 +1335,7 @@ "outputs": [], "source": [ "# GridPlot of shape 2 x 3 with all controllers synced\n", - "grid_plot = fpl.GridPlot(shape=(2, 3), controllers=\"sync\")\n", + "grid_plot = fpl.GridPlot(shape=(2, 3), controller_ids=\"sync\")\n", "\n", "# Make a random image graphic for each subplot\n", "for subplot in grid_plot:\n", @@ -1549,7 +1523,7 @@ "# pan-zoom controllers for each view\n", "# views are synced if they have the \n", "# same controller ID\n", - "controllers = [\n", + "controller_ids = [\n", " [0, 3, 1], # id each controller with an integer\n", " [2, 2, 3]\n", "]\n", @@ -1564,7 +1538,7 @@ "# Create the grid plot\n", "grid_plot = fpl.GridPlot(\n", " shape=grid_shape,\n", - " controllers=controllers,\n", + " controller_ids=controller_ids,\n", " names=names,\n", ")\n", "\n", @@ -1678,18 +1652,6 @@ "grid_plot[1, 0][\"rand-image\"].vim = 0.1\n", "grid_plot[1, 0][\"rand-image\"].vmax = 0.3" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8a5753b9-ee71-4ed1-bb0d-52bdb4ea365f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "grid_plot[1, 0][\"rand-image\"].type" - ] } ], "metadata": { From 50526298e6713a6d75968d5c65fd9aeddcb364b7 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 06:13:52 -0500 Subject: [PATCH 12/17] update screenshot --- .../notebooks/screenshots/nb-image-widget-movie-set_data.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index 33798861e..61c3aeb8c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df2f6e73fdb0946d915f40df1e27f2ae89ba6c6e5d0dadb84406fe8b753735ab -size 34974 +oid sha256:60318615e4850d37a4ffae16ca1e3bbf2985ddafd0dd65ba6fae997e1d123d67 +size 31251 From 69636792c72ab2ef340beccccd630e2fe3bd4dd1 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 26 Nov 2023 22:17:09 -0500 Subject: [PATCH 13/17] use perspective camera for everything --- fastplotlib/layouts/_gridplot.py | 5 ++-- fastplotlib/layouts/_plot.py | 4 +-- fastplotlib/layouts/_plot_area.py | 42 +++++++++++++++++++++---------- fastplotlib/layouts/_subplot.py | 11 +++++--- fastplotlib/layouts/_utils.py | 36 +++++++++++++------------- 5 files changed, 58 insertions(+), 40 deletions(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index a8f370875..612bee90e 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -47,11 +47,12 @@ def __init__( cameras: str, list, or np.ndarray, optional | One of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots - | OR | list/array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot + | list/array of pygfx.PerspectiveCamera instances controller_types: str, list or np.ndarray, optional - list or array that specifies the controller type for each subplot. + list or array that specifies the controller type for each subplot, or list/array of + pygfx.Controller instances controller_ids: str, list or np.ndarray of int or str ids, optional | If `None` a unique controller is created for each subplot diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 2937dc27b..34027a276 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -13,7 +13,7 @@ def __init__( self, canvas: Union[str, WgpuCanvas] = None, renderer: pygfx.WgpuRenderer = None, - camera: Union[str, pygfx.Camera] = "2d", + camera: Union[str, pygfx.PerspectiveCamera] = "2d", controller: Union[str, pygfx.Controller] = None, size: Tuple[int, int] = (500, 300), **kwargs, @@ -29,7 +29,7 @@ def __init__( renderer: pygfx.Renderer, optional pygfx renderer instance - camera: str or pygfx.Camera, optional + camera: str or pygfx.PerspectiveCamera, optional | One of ``"2d"`` or ``"3d"`` indicating 2D or 3D camera controller: str or pygfx.Controller, optional diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index b0b4b137b..9522832d3 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -25,7 +25,7 @@ def __init__( self, parent, position: Any, - camera: Union[pygfx.Camera], + camera: Union[pygfx.PerspectiveCamera], controller: Union[pygfx.Controller], scene: pygfx.Scene, canvas: WgpuCanvas, @@ -45,8 +45,8 @@ def __init__( typical use will be for ``subplots`` in a ``gridplot``, position would correspond to the ``[row, column]`` location of the ``subplot`` in its ``gridplot`` - camera: pygfx.Camera - ``pygfx.OrthographicCamera`` is used for 2D scenes ``pygfx.PerspectiveCamera`` is used to view 3D scenes + camera: pygfx.PerspectiveCamera + Use perspective camera for both perspective and orthographic views. Set fov = 0 for orthographic mode. controller: pygfx.Controller One of the pygfx controllers, panzoom, fly, orbit, or trackball @@ -136,20 +136,35 @@ def viewport(self) -> pygfx.Viewport: return self._viewport @property - def camera(self) -> pygfx.Camera: + def camera(self) -> pygfx.PerspectiveCamera: """camera used to view the scene""" return self._camera @camera.setter - def camera(self, new_camera: Union[str, pygfx.Camera]): - new_camera = create_camera(new_camera) + def camera(self, new_camera: Union[str, pygfx.PerspectiveCamera]): + # user wants to set completely new camera, remove current camera from controller + if isinstance(new_camera, pygfx.PerspectiveCamera): + self.controller.remove_camera(self._camera) + # add new camera to controller + self.controller.add_camera(new_camera) + + self._camera = new_camera + + # modify FOV if necessary + elif isinstance(new_camera, str): + if new_camera == "2d": + self._camera.fov = 0 + + elif new_camera == "3d": + # orthographic -> perspective only if fov = 0, i.e. if camera is in ortho mode + # otherwise keep same FOV + if self._camera.fov == 0: + self._camera.fov = 50 - # remove current camera from controller - self.controller.remove_camera(self._camera) - # add new camera to controller - self.controller.add_camera(new_camera) - - self._camera = new_camera + else: + raise ValueError("camera must be one of '2d', '3d' or a pygfx.PerspectiveCamera instance") + else: + raise ValueError("camera must be one of '2d', '3d' or a pygfx.PerspectiveCamera instance") # in the future we can think about how to allow changing the controller @property @@ -176,7 +191,8 @@ def controller(self, new_controller: Union[str, pygfx.Controller]): self.viewport ) - # monkeypatch until we figure out a better way + # TODO: monkeypatch until we figure out a better + # pygfx plans on refactoring viewports anyways if self.parent is not None: if self.parent.__class__.__name__ == "GridPlot": for subplot in self.parent: diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 99d804aea..e9eae7603 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -18,7 +18,7 @@ def __init__( parent: Any = None, position: Tuple[int, int] = None, parent_dims: Tuple[int, int] = None, - camera: Union[str, pygfx.Camera] = "2d", + camera: Union[str, pygfx.PerspectiveCamera] = "2d", controller: Union[str, pygfx.Controller] = None, canvas: Union[str, WgpuCanvas, pygfx.Texture] = None, renderer: pygfx.WgpuRenderer = None, @@ -33,15 +33,18 @@ def __init__( Parameters ---------- + parent: Any + parent GridPlot instance + position: (int, int), optional corresponds to the [row, column] position of the subplot within a ``Gridplot`` parent_dims: (int, int), optional dimensions of the parent ``GridPlot`` - camera: str or pygfx.Camera, default '2d' - indicates the kind of pygfx camera that will be instantiated, '2d' uses pygfx ``OrthographicCamera`` and - '3d' uses pygfx ``PerspectiveCamera`` + camera: str or pygfx.PerspectiveCamera, default '2d' + indicates the FOV for the camera, '2d' sets ``fov = 0``, '3d' sets ``fov = 50``. + ``fov`` can be changed at any time. controller: str or pygfx.Controller, optional | if ``None``, uses a PanZoomController for "2d" camera or FlyController for "3d" camera. diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 115eac9f7..7db1d84c4 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -91,25 +91,23 @@ def make_canvas_and_renderer( return canvas, renderer -camera_types = { - "2d": pygfx.OrthographicCamera, - "3d": pygfx.PerspectiveCamera, -} - - def create_camera( - camera_type: Union[pygfx.Camera, str], -) -> Union[pygfx.OrthographicCamera, pygfx.PerspectiveCamera]: - if isinstance(camera_type, (pygfx.OrthographicCamera, pygfx.PerspectiveCamera)): + camera_type: Union[pygfx.PerspectiveCamera, str], +) -> pygfx.PerspectiveCamera: + if isinstance(camera_type, pygfx.PerspectiveCamera): return camera_type - if camera_type not in camera_types.keys(): - raise KeyError( - f"camera must be a valid pygfx.Camera or one of: " - f"{list(camera_types.keys())}, you have passed: {camera_type}" - ) + elif camera_type == "2d": + # use perspective for orthographic, makes it easier to then switch to controllers that make sense with fov > 0 + return pygfx.PerspectiveCamera(fov=0) - return camera_types[camera_type]() + elif camera_type == "3d": + return pygfx.PerspectiveCamera() + + else: + raise ValueError( + "camera must be one of: '2d', '3d' or an instance of pygfx.PerspectiveCamera" + ) controller_types = { @@ -122,7 +120,7 @@ def create_camera( def create_controller( controller_type: Union[pygfx.Controller, None, str], - camera: Union[pygfx.Camera], + camera: pygfx.PerspectiveCamera, ) -> pygfx.Controller: """ Creates the controllers and adds the camera to it. @@ -133,10 +131,10 @@ def create_controller( if controller_type is None: # default controllers - if camera == "2d" or isinstance(camera, pygfx.OrthographicCamera): + if camera.fov == 0: + # default for orthographic return pygfx.PanZoomController(camera) - - elif camera == "3d" or isinstance(camera, pygfx.PerspectiveCamera): + else: return pygfx.FlyController(camera) if controller_type not in controller_types.keys(): From 6cec1e858695765f6247359dde788c5396be1648 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 29 Nov 2023 12:33:53 -0500 Subject: [PATCH 14/17] typo Co-authored-by: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> --- fastplotlib/layouts/_gridplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 612bee90e..bcdd5bdcd 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -65,7 +65,7 @@ def __init__( | sync first 2 plots, and sync last 2 plots: [[0, 0, 1], [2, 3, 3]] | Example with str subplot names: | list of lists of subplot names, each sublist is synced: [[subplot_a, subplot_b], [subplot_f, subplot_c]] - | this syncs subplot_a and subplot_b together; syncs subplot_e and subplot_c together + | this syncs subplot_a and subplot_b together; syncs subplot_f and subplot_c together canvas: WgpuCanvas, optional Canvas for drawing From 91c9dfc7e3b53c6ed2a2f9f5bbb88c46e321d880 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 29 Nov 2023 12:42:06 -0500 Subject: [PATCH 15/17] typo Co-authored-by: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> --- fastplotlib/layouts/_gridplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index bcdd5bdcd..fa56377f8 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -199,7 +199,7 @@ def __init__( cams = self._cameras[controller_ids == cid].ravel() if cont_type == "default": - # hacky fix for now because of hwo `create_controller()` works + # hacky fix for now because of how `create_controller()` works cont_type = None _controller = create_controller(controller_type=cont_type, camera=cams[0]) From c9c233971f4c27faf5eca3ed437adb49bea99b78 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 29 Nov 2023 12:56:50 -0500 Subject: [PATCH 16/17] more detailed exception --- fastplotlib/layouts/_gridplot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index fa56377f8..926788445 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -116,6 +116,11 @@ def __init__( elif isinstance(controller_ids, str): if controller_ids == "sync": controller_ids = np.zeros(self.shape, dtype=int) + else: + raise ValueError( + f"`controller_ids` must be one of 'sync', an array/list of subplot names, or an array/list of " + f"integer ids. See the docstring for more details." + ) # list controller_ids elif isinstance(controller_ids, (list, np.ndarray)): From 071bef4f4b1dd7bc23ab803b46df71bd26edbe48 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 29 Nov 2023 12:58:35 -0500 Subject: [PATCH 17/17] better names --- fastplotlib/layouts/_gridplot.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 926788445..459aca5fd 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -124,20 +124,20 @@ def __init__( # list controller_ids elif isinstance(controller_ids, (list, np.ndarray)): - flat = list(chain(*controller_ids)) + ids_flat = list(chain(*controller_ids)) # list of str of subplot names, convert this to integer ids - if all([isinstance(item, str) for item in flat]): + if all([isinstance(item, str) for item in ids_flat]): if self.names is None: raise ValueError("must specify subplot `names` to use list of str for `controller_ids`") # make sure each controller_id str is a subplot name - if not all([n in self.names for n in flat]): + if not all([n in self.names for n in ids_flat]): raise KeyError( f"all `controller_ids` strings must be one of the subplot names" ) - if len(flat) > len(set(flat)): + if len(ids_flat) > len(set(ids_flat)): raise ValueError( "id strings must not appear twice in `controller_ids`" ) @@ -153,7 +153,7 @@ def __init__( controller_ids = ids_init # integer ids - elif all([isinstance(item, (int, np.integer)) for item in flat]): + elif all([isinstance(item, (int, np.integer)) for item in ids_flat]): controller_ids = to_array(controller_ids).reshape(self.shape) else: @@ -170,13 +170,13 @@ def __init__( controller_types = np.array(["default"] * self.shape[0] * self.shape[1]).reshape(self.shape) # validate controller types - flat = list(chain(*controller_types)) + types_flat = list(chain(*controller_types)) # str controller_type or pygfx instances valid_str = list(valid_controller_types.keys()) + ["default"] valid_instances = tuple(valid_controller_types.values()) # make sure each controller type is valid - for controller_type in flat: + for controller_type in types_flat: if controller_type is None: continue