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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy