From 0802f34c5df280842d9ab52266beee56092225ed Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 27 Feb 2025 00:08:34 -0500 Subject: [PATCH 01/82] start basic mesh and camera stuff --- fastplotlib/layouts/_subplot_bbox.py | 96 ++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 fastplotlib/layouts/_subplot_bbox.py diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py new file mode 100644 index 000000000..79ccc5e18 --- /dev/null +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -0,0 +1,96 @@ +import fastplotlib as fpl +import pygfx +import numpy as np + + +class UnderlayCamera(pygfx.Camera): + """ + Same as pygfx.ScreenCoordsCamera but y-axis is inverted. + + So top right is (0, 0). This is easier to manage because we + often resize using the bottom right corner. + """ + + def _update_projection_matrix(self): + width, height = self._view_size + sx, sy, sz = 2 / width, 2 / height, 1 + dx, dy, dz = -1, 1, 0 # pygfx is -1, -1, 0 + m = sx, 0, 0, dx, 0, sy, 0, dy, 0, 0, sz, dz, 0, 0, 0, 1 + proj_matrix = np.array(m, dtype=float).reshape(4, 4) + proj_matrix.flags.writeable = False + return proj_matrix + + +""" +Each subplot is defined by a 2D plane mesh, a rectangle. +The rectangles are viewed using the UnderlayCamera where (0, 0) is the top left corner. +We can control the bbox of this rectangle by changing the x and y boundaries of the rectangle. + +Note how the y values are negative. + +Illustration: + +(0, 0) --------------------------------------------------- +---------------------------------------------------------- +---------------------------------------------------------- +--------------(x1, -y1) --------------- (x2, -y1) -------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||rectangle|||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +--------------(x1, -y2) --------------- (x2, -y2)--------- +---------------------------------------------------------- +------------------------------------------- (canvas_width, canvas_height) + +""" + + +class MeshMasks: + """Used set the x1, x2, y1, y2 positions of the mesh""" + x1 = np.array([ + [False, False, False], + [True, False, False], + [False, False, False], + [True, False, False], + ]) + + x2 = np.array([ + [True, False, False], + [False, False, False], + [True, False, False], + [False, False, False], + ]) + + y1 = np.array([ + [False, True, False], + [False, True, False], + [False, False, False], + [False, False, False], + ]) + + y2 = np.array([ + [False, False, False], + [False, False, False], + [False, True, False], + [False, True, False], + ]) + + +masks = MeshMasks + + +def make_mesh(x1, y1, w, h): + geometry = pygfx.plane_geometry(1, 1) + material = pygfx.MeshBasicMaterial() + plane = pygfx.Mesh(geometry, material) + + plane.geometry.positions.data[masks.x1] = x1 + plane.geometry.positions.data[masks.x2] = x1 + w + plane.geometry.positions.data[masks.y1] = -y1 # negative because UnderlayCamera y is inverted + plane.geometry.positions.data[masks.y2] = -(y1 + h) + + plane.geometry.positions.update_full() + return plane \ No newline at end of file From c26da10d58246f788628d67cc93cc6442f3e7e46 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 27 Feb 2025 00:46:05 -0500 Subject: [PATCH 02/82] progress --- fastplotlib/layouts/_subplot_bbox.py | 103 +++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 12 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index 79ccc5e18..0e1bef1ba 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -82,15 +82,94 @@ class MeshMasks: masks = MeshMasks -def make_mesh(x1, y1, w, h): - geometry = pygfx.plane_geometry(1, 1) - material = pygfx.MeshBasicMaterial() - plane = pygfx.Mesh(geometry, material) - - plane.geometry.positions.data[masks.x1] = x1 - plane.geometry.positions.data[masks.x2] = x1 + w - plane.geometry.positions.data[masks.y1] = -y1 # negative because UnderlayCamera y is inverted - plane.geometry.positions.data[masks.y2] = -(y1 + h) - - plane.geometry.positions.update_full() - return plane \ No newline at end of file +class SubplotFrame: + def __init__(self, bbox: tuple): + x1, y1, w, h = bbox + geometry = pygfx.plane_geometry(1, 1) + material = pygfx.MeshBasicMaterial() + self._plane = pygfx.Mesh(geometry, material) + + self._resize_handler = pygfx.Points( + pygfx.Geometry(positions=[[x1, -y1, 0]]), + pygfx.PointsMarkerMaterial(marker="square", size=4, size_space="screen") + ) + + self.rect = bbox + + self._world_object = pygfx.Group() + self._world_object.add(self._plane, self._resize_handler) + + def validate_bbox(self, bbox): + for val, name in zip(bbox, ["x-position", "y-position", "width", "height"]): + if val < 0: + raise ValueError(f"Invalid bbox value < 0 for: {name}") + + @property + def rect(self) -> tuple[float, float, float, float]: + x = self.plane.geometry.positions.data[masks.x1][0] + y = self.plane.geometry.positions.data[masks.y1][0] + + w = self.plane.geometry.positions.data[masks.x2][0] - x + h = self.plane.geometry.positions.data[masks.y2][0] - y + + return x, -y, w, -h # remember y is inverted + + @rect.setter + def rect(self, bbox: tuple[float, float, float, float]): + self.validate_bbox(bbox) + x1, y1, w, h = bbox + + x2 = x1 + w + y2 = y1 + h + + self._plane.geometry.positions.data[masks.x1] = x1 + self._plane.geometry.positions.data[masks.x2] = x2 + self._plane.geometry.positions.data[masks.y1] = -y1 # negative because UnderlayCamera y is inverted + self._plane.geometry.positions.data[masks.y2] = -y2 + + self._plane.geometry.positions.update_full() + + self._resize_handler.geometry.positions.data[0] = [x2, -y2, 0] + self._resize_handler.geometry.positions.update_full() + + @property + def plane(self) -> pygfx.Mesh: + return self._plane + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From f199f65ac3e2665b20be9515cea9401b0660bcee Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 28 Feb 2025 01:58:52 -0500 Subject: [PATCH 03/82] resizing canvas auto-resizes bboxes using internal fractional bbox --- fastplotlib/layouts/_subplot_bbox.py | 133 +++++++++++++++++++++------ 1 file changed, 106 insertions(+), 27 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index 0e1bef1ba..b94e3ff9b 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -1,4 +1,3 @@ -import fastplotlib as fpl import pygfx import numpy as np @@ -33,7 +32,7 @@ def _update_projection_matrix(self): (0, 0) --------------------------------------------------- ---------------------------------------------------------- ---------------------------------------------------------- ---------------(x1, -y1) --------------- (x2, -y1) -------- +--------------(x0, -y0) --------------- (x1, -y0) -------- ------------------------|||||||||||||||------------------- ------------------------|||||||||||||||------------------- ------------------------|||||||||||||||------------------- @@ -41,7 +40,7 @@ def _update_projection_matrix(self): ------------------------|||||||||||||||------------------- ------------------------|||||||||||||||------------------- ------------------------|||||||||||||||------------------- ---------------(x1, -y2) --------------- (x2, -y2)--------- +--------------(x0, -y1) --------------- (x1, -y1)--------- ---------------------------------------------------------- ------------------------------------------- (canvas_width, canvas_height) @@ -49,29 +48,29 @@ def _update_projection_matrix(self): class MeshMasks: - """Used set the x1, x2, y1, y2 positions of the mesh""" - x1 = np.array([ + """Used set the x1, x1, y0, y1 positions of the mesh""" + x0 = np.array([ [False, False, False], [True, False, False], [False, False, False], [True, False, False], ]) - x2 = np.array([ + x1 = np.array([ [True, False, False], [False, False, False], [True, False, False], [False, False, False], ]) - y1 = np.array([ + y0 = np.array([ [False, True, False], [False, True, False], [False, False, False], [False, False, False], ]) - y2 = np.array([ + y1 = np.array([ [False, False, False], [False, False, False], [False, True, False], @@ -83,14 +82,22 @@ class MeshMasks: class SubplotFrame: - def __init__(self, bbox: tuple): - x1, y1, w, h = bbox + def __init__(self, figure, bbox: np.ndarray = None, ranges: np.ndarray = None): + self.figure = figure + self._canvas_rect = figure.get_pygfx_render_area() + figure.canvas.add_event_handler(self._canvas_resized, "resize") + + bbox = self._get_bbox_screen_coords(bbox) + + x0, y0, w, h = bbox + self._bbox_screen = np.array(bbox, dtype=np.int64) + geometry = pygfx.plane_geometry(1, 1) material = pygfx.MeshBasicMaterial() self._plane = pygfx.Mesh(geometry, material) self._resize_handler = pygfx.Points( - pygfx.Geometry(positions=[[x1, -y1, 0]]), + pygfx.Geometry(positions=[[x0, -y0, 0]]), pygfx.PointsMarkerMaterial(marker="square", size=4, size_space="screen") ) @@ -99,45 +106,117 @@ def __init__(self, bbox: tuple): self._world_object = pygfx.Group() self._world_object.add(self._plane, self._resize_handler) - def validate_bbox(self, bbox): + def _get_bbox_screen_coords(self, bbox) -> np.ndarray[int]: for val, name in zip(bbox, ["x-position", "y-position", "width", "height"]): if val < 0: raise ValueError(f"Invalid bbox value < 0 for: {name}") + bbox = np.asarray(bbox) + + _, _, cw, ch = self._canvas_rect + mult = np.array([cw, ch, cw, ch]) + + if (bbox < 1).all(): # fractional bbox + # check that widths, heights are valid: + if bbox[0] + bbox[2] > 1: + raise ValueError("invalid fractional value: x + width > 1") + if bbox[1] + bbox[3] > 1: + raise ValueError("invalid fractional value: y + height > 1") + + self._bbox_frac = bbox.copy() + bbox = self._bbox_frac * mult + + elif (bbox > 1).all(): # bbox in already in screen coords coordinates + # check that widths, heights are valid + if bbox[0] + bbox[2] > cw: + raise ValueError("invalid value: x + width > 1") + if bbox[1] + bbox[3] > ch: + raise ValueError("invalid value: y + height > 1") + + self._bbox_frac = bbox / mult + + return bbox.astype(np.int64) + + @property + def x(self) -> tuple[np.int64, np.int64]: + return self.rect[0], self.rect[0] + self.rect[2] + + @property + def y(self) -> tuple[np.int64, np.int64]: + return self.rect[1], self.rect[1] + self.rect[3] + @property - def rect(self) -> tuple[float, float, float, float]: - x = self.plane.geometry.positions.data[masks.x1][0] - y = self.plane.geometry.positions.data[masks.y1][0] + def x_frac(self): + pass - w = self.plane.geometry.positions.data[masks.x2][0] - x - h = self.plane.geometry.positions.data[masks.y2][0] - y + @property + def y_frac(self): + pass - return x, -y, w, -h # remember y is inverted + @property + def rect(self) -> np.ndarray[int]: + return self._bbox_screen @rect.setter - def rect(self, bbox: tuple[float, float, float, float]): - self.validate_bbox(bbox) - x1, y1, w, h = bbox + def rect(self, bbox: np.ndarray): + bbox = self._get_bbox_screen_coords(bbox) + self._set_plane(bbox) + + def _set_plane(self, bbox: np.ndarray): + """bbox is in screen coordinates, not fractional""" - x2 = x1 + w - y2 = y1 + h + x0, y0, w, h = bbox + x1 = x0 + w + y1 = y0 + h + + self._plane.geometry.positions.data[masks.x0] = x0 self._plane.geometry.positions.data[masks.x1] = x1 - self._plane.geometry.positions.data[masks.x2] = x2 - self._plane.geometry.positions.data[masks.y1] = -y1 # negative because UnderlayCamera y is inverted - self._plane.geometry.positions.data[masks.y2] = -y2 + self._plane.geometry.positions.data[masks.y0] = -y0 # negative because UnderlayCamera y is inverted + self._plane.geometry.positions.data[masks.y1] = -y1 self._plane.geometry.positions.update_full() - self._resize_handler.geometry.positions.data[0] = [x2, -y2, 0] + self._resize_handler.geometry.positions.data[0] = [x1, -y1, 0] self._resize_handler.geometry.positions.update_full() + self._bbox_screen[:] = bbox + @property def plane(self) -> pygfx.Mesh: return self._plane + def _canvas_resized(self, *ev): + # render area, to account for any edge windows that might be present + # remember this frame also encapsulates the imgui toolbar which is + # part of the subplot so we do not subtract the toolbar height! + self._canvas_rect = self.figure.get_pygfx_render_area() + + # set rect using existing fractional bbox + self.rect = self._bbox_frac + + def __repr__(self): + s = f"{self._bbox_frac}\n{self.rect}" + + return s + + +class FlexLayoutManager: + def __init__(self, figure, *frames: SubplotFrame): + self.figure = figure + self.figure.renderer.add_event_handler(self._figure_resized, "resize") + + # for subplot in + + def _subplot_changed(self): + """ + Check that this subplot x_range, y_range does not overlap with any other + Check that this x_min > all other x_ + """ + def _figure_resized(self, ev): + w, h = ev["width"], ev["height"] From f9b5c144e403746d5e9e85de5b07d2fab490e8f3 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 28 Feb 2025 02:36:19 -0500 Subject: [PATCH 04/82] resizing works well --- fastplotlib/layouts/_subplot_bbox.py | 119 +++++++++++++++++---------- 1 file changed, 76 insertions(+), 43 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index b94e3ff9b..516cc27e4 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -82,15 +82,34 @@ class MeshMasks: class SubplotFrame: - def __init__(self, figure, bbox: np.ndarray = None, ranges: np.ndarray = None): + def __init__(self, figure, rect: np.ndarray = None, ranges: np.ndarray = None): + """ + + Parameters + ---------- + figure + rect: (x, y, w, h) + in absolute screen space or fractional screen space, example if the canvas w, h is (100, 200) + a fractional rect of (0.1, 0.1, 0.5, 0.5) is (10, 10, 50, 100) in absolute screen space + + ranges (xmin, xmax, ymin, ymax) + in absolute screen coordinates or fractional screen coordinates + """ self.figure = figure self._canvas_rect = figure.get_pygfx_render_area() figure.canvas.add_event_handler(self._canvas_resized, "resize") - bbox = self._get_bbox_screen_coords(bbox) + self._rect_frac = np.zeros(4, dtype=np.float64) + self._rect_screen_space = np.zeros(4, dtype=np.float64) + + if rect is None: + if ranges is None: + raise ValueError + rect = self._ranges_to_rect(ranges) - x0, y0, w, h = bbox - self._bbox_screen = np.array(bbox, dtype=np.int64) + self._assign_rect(rect) + + x0, y0, w, h = self.rect geometry = pygfx.plane_geometry(1, 1) material = pygfx.MeshBasicMaterial() @@ -101,71 +120,86 @@ def __init__(self, figure, bbox: np.ndarray = None, ranges: np.ndarray = None): pygfx.PointsMarkerMaterial(marker="square", size=4, size_space="screen") ) - self.rect = bbox + self._reset_plane() self._world_object = pygfx.Group() self._world_object.add(self._plane, self._resize_handler) - def _get_bbox_screen_coords(self, bbox) -> np.ndarray[int]: - for val, name in zip(bbox, ["x-position", "y-position", "width", "height"]): + def _ranges_to_rect(self, ranges) -> np.ndarray: + """convert ranges to rect""" + x0, x1, y0, y1 = ranges + + # width and height + w = x1 - x0 + h = y1 - y0 + + if x1 - x0 <= 0: + raise ValueError + if y1 - y0 <= 0: + raise ValueError + + x, y, w, h = x0, y0, w, h + + return np.array([x, y, w, h]) + + def _assign_rect(self, rect) -> np.ndarray[int]: + for val, name in zip(rect, ["x-position", "y-position", "width", "height"]): if val < 0: - raise ValueError(f"Invalid bbox value < 0 for: {name}") + raise ValueError(f"Invalid rect value < 0 for: {name}") - bbox = np.asarray(bbox) + rect = np.asarray(rect) _, _, cw, ch = self._canvas_rect mult = np.array([cw, ch, cw, ch]) - if (bbox < 1).all(): # fractional bbox + if (rect[2:] <= 1).all(): # fractional bbox # check that widths, heights are valid: - if bbox[0] + bbox[2] > 1: + if rect[0] + rect[2] > 1: raise ValueError("invalid fractional value: x + width > 1") - if bbox[1] + bbox[3] > 1: + if rect[1] + rect[3] > 1: raise ValueError("invalid fractional value: y + height > 1") - self._bbox_frac = bbox.copy() - bbox = self._bbox_frac * mult + # assign values, don't just change the reference + self._rect_frac[:] = rect + self._rect_screen_space[:] = self._rect_frac * mult - elif (bbox > 1).all(): # bbox in already in screen coords coordinates + # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 + elif (rect[2:] > 1).all(): # bbox in already in screen coords coordinates # check that widths, heights are valid - if bbox[0] + bbox[2] > cw: + if rect[0] + rect[2] > cw: raise ValueError("invalid value: x + width > 1") - if bbox[1] + bbox[3] > ch: + if rect[1] + rect[3] > ch: raise ValueError("invalid value: y + height > 1") - self._bbox_frac = bbox / mult + self._rect_frac[:] = rect / mult + self._rect_screen_space[:] = rect - return bbox.astype(np.int64) + else: + raise ValueError(f"Invalid rect: {rect}") @property - def x(self) -> tuple[np.int64, np.int64]: - return self.rect[0], self.rect[0] + self.rect[2] + def ranges(self) -> tuple[np.int64, np.int64, np.int64, np.int64]: + return self.rect[0], self.rect[0] + self.rect[2], self.rect[1], self.rect[1] + self.rect[3] - @property - def y(self) -> tuple[np.int64, np.int64]: - return self.rect[1], self.rect[1] + self.rect[3] - - @property - def x_frac(self): - pass - - @property - def y_frac(self): - pass + @ranges.setter + def ranges(self, ranges: np.ndarray): + rect = self._ranges_to_rect(ranges) + self.rect = rect @property def rect(self) -> np.ndarray[int]: - return self._bbox_screen + """rect in absolute screen space""" + return self._rect_screen_space @rect.setter - def rect(self, bbox: np.ndarray): - bbox = self._get_bbox_screen_coords(bbox) - self._set_plane(bbox) + def rect(self, rect: np.ndarray): + self._assign_rect(rect) + self._reset_plane() - def _set_plane(self, bbox: np.ndarray): + def _reset_plane(self): """bbox is in screen coordinates, not fractional""" - x0, y0, w, h = bbox + x0, y0, w, h = self.rect x1 = x0 + w y1 = y0 + h @@ -180,8 +214,6 @@ def _set_plane(self, bbox: np.ndarray): self._resize_handler.geometry.positions.data[0] = [x1, -y1, 0] self._resize_handler.geometry.positions.update_full() - self._bbox_screen[:] = bbox - @property def plane(self) -> pygfx.Mesh: return self._plane @@ -192,11 +224,11 @@ def _canvas_resized(self, *ev): # part of the subplot so we do not subtract the toolbar height! self._canvas_rect = self.figure.get_pygfx_render_area() - # set rect using existing fractional bbox - self.rect = self._bbox_frac + # set rect using existing rect_frac since this remains constant regardless of resize + self.rect = self._rect_frac def __repr__(self): - s = f"{self._bbox_frac}\n{self.rect}" + s = f"{self._rect_frac}\n{self.rect}" return s @@ -250,5 +282,6 @@ def _figure_resized(self, ev): + From c1100b1adb5cfdd477b39ad56e69aa2d283c4836 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 28 Feb 2025 02:55:28 -0500 Subject: [PATCH 05/82] ranges as array, comments --- fastplotlib/layouts/_subplot_bbox.py | 39 ++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index 516cc27e4..c8099f5df 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -99,24 +99,31 @@ def __init__(self, figure, rect: np.ndarray = None, ranges: np.ndarray = None): self._canvas_rect = figure.get_pygfx_render_area() figure.canvas.add_event_handler(self._canvas_resized, "resize") + # initialize rect state arrays + # used to store internal state of the rect in both fractional screen space and absolute screen space + # the purpose of storing the fractional rect is that it remains constant when the canvas resizes self._rect_frac = np.zeros(4, dtype=np.float64) self._rect_screen_space = np.zeros(4, dtype=np.float64) if rect is None: if ranges is None: - raise ValueError + raise ValueError("Must provide rect or ranges") + + # convert ranges to rect rect = self._ranges_to_rect(ranges) + # assign the internal state of the rect by parsing the user passed rect self._assign_rect(rect) - x0, y0, w, h = self.rect - + # init mesh of size 1 to graphically represent rect geometry = pygfx.plane_geometry(1, 1) material = pygfx.MeshBasicMaterial() self._plane = pygfx.Mesh(geometry, material) + # create resize handler at point (x1, y1) + x1, y1 = self.ranges[[1, 3]] self._resize_handler = pygfx.Points( - pygfx.Geometry(positions=[[x0, -y0, 0]]), + pygfx.Geometry(positions=[[x1, -y1, 0]]), # y is inverted in UnderlayCamera pygfx.PointsMarkerMaterial(marker="square", size=4, size_space="screen") ) @@ -143,6 +150,10 @@ def _ranges_to_rect(self, ranges) -> np.ndarray: return np.array([x, y, w, h]) def _assign_rect(self, rect) -> np.ndarray[int]: + """ + Using the passed rect which is either absolute screen space or fractional, + set the internal fractional and absolute screen space rects + """ for val, name in zip(rect, ["x-position", "y-position", "width", "height"]): if val < 0: raise ValueError(f"Invalid rect value < 0 for: {name}") @@ -178,8 +189,10 @@ def _assign_rect(self, rect) -> np.ndarray[int]: raise ValueError(f"Invalid rect: {rect}") @property - def ranges(self) -> tuple[np.int64, np.int64, np.int64, np.int64]: - return self.rect[0], self.rect[0] + self.rect[2], self.rect[1], self.rect[1] + self.rect[3] + def ranges(self) -> np.ndarray: + """ranges, (xmin, xmax, ymin, ymax)""" + # not actually stored, computed when needed + return np.asarray([self.rect[0], self.rect[0] + self.rect[2], self.rect[1], self.rect[1] + self.rect[3]]) @ranges.setter def ranges(self, ranges: np.ndarray): @@ -188,7 +201,7 @@ def ranges(self, ranges: np.ndarray): @property def rect(self) -> np.ndarray[int]: - """rect in absolute screen space""" + """rect in absolute screen space, (x, y, w, h)""" return self._rect_screen_space @rect.setter @@ -197,7 +210,7 @@ def rect(self, rect: np.ndarray): self._reset_plane() def _reset_plane(self): - """bbox is in screen coordinates, not fractional""" + """reset the plane mesh using the current rect state""" x0, y0, w, h = self.rect @@ -206,25 +219,28 @@ def _reset_plane(self): self._plane.geometry.positions.data[masks.x0] = x0 self._plane.geometry.positions.data[masks.x1] = x1 - self._plane.geometry.positions.data[masks.y0] = -y0 # negative because UnderlayCamera y is inverted + self._plane.geometry.positions.data[masks.y0] = -y0 # negative y because UnderlayCamera y is inverted self._plane.geometry.positions.data[masks.y1] = -y1 self._plane.geometry.positions.update_full() + # note the negative y because UnderlayCamera y is inverted self._resize_handler.geometry.positions.data[0] = [x1, -y1, 0] self._resize_handler.geometry.positions.update_full() @property def plane(self) -> pygfx.Mesh: + """the plane mesh""" return self._plane def _canvas_resized(self, *ev): + """triggered when canvas is resized""" # render area, to account for any edge windows that might be present # remember this frame also encapsulates the imgui toolbar which is # part of the subplot so we do not subtract the toolbar height! self._canvas_rect = self.figure.get_pygfx_render_area() - # set rect using existing rect_frac since this remains constant regardless of resize + # set new rect using existing rect_frac since this remains constant regardless of resize self.rect = self._rect_frac def __repr__(self): @@ -238,7 +254,7 @@ def __init__(self, figure, *frames: SubplotFrame): self.figure = figure self.figure.renderer.add_event_handler(self._figure_resized, "resize") - # for subplot in + self._frames = frames def _subplot_changed(self): """ @@ -246,6 +262,7 @@ def _subplot_changed(self): Check that this x_min > all other x_ """ + pass def _figure_resized(self, ev): w, h = ev["width"], ev["height"] From d7f572e8862149984b979d3978ee5c3180f514d9 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 1 Mar 2025 04:40:17 -0500 Subject: [PATCH 06/82] layout management logic works! :D git status --- fastplotlib/layouts/_subplot_bbox.py | 260 ++++++++++++++++++++------- 1 file changed, 199 insertions(+), 61 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index c8099f5df..f250b5e31 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -1,5 +1,7 @@ -import pygfx +from functools import partial + import numpy as np +import pygfx class UnderlayCamera(pygfx.Camera): @@ -82,7 +84,7 @@ class MeshMasks: class SubplotFrame: - def __init__(self, figure, rect: np.ndarray = None, ranges: np.ndarray = None): + def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None): """ Parameters @@ -92,10 +94,12 @@ def __init__(self, figure, rect: np.ndarray = None, ranges: np.ndarray = None): in absolute screen space or fractional screen space, example if the canvas w, h is (100, 200) a fractional rect of (0.1, 0.1, 0.5, 0.5) is (10, 10, 50, 100) in absolute screen space - ranges (xmin, xmax, ymin, ymax) - in absolute screen coordinates or fractional screen coordinates + extent: (xmin, xmax, ymin, ymax) + range in absolute screen coordinates or fractional screen coordinates """ self.figure = figure + + # canvas (x, y, w, h) self._canvas_rect = figure.get_pygfx_render_area() figure.canvas.add_event_handler(self._canvas_resized, "resize") @@ -106,25 +110,28 @@ def __init__(self, figure, rect: np.ndarray = None, ranges: np.ndarray = None): self._rect_screen_space = np.zeros(4, dtype=np.float64) if rect is None: - if ranges is None: + if extent is None: raise ValueError("Must provide rect or ranges") + valid, error = self._validate_extent(extent) + if not valid: + raise ValueError(error) # convert ranges to rect - rect = self._ranges_to_rect(ranges) + rect = self._extent_to_rect(extent) # assign the internal state of the rect by parsing the user passed rect self._assign_rect(rect) # init mesh of size 1 to graphically represent rect geometry = pygfx.plane_geometry(1, 1) - material = pygfx.MeshBasicMaterial() + material = pygfx.MeshBasicMaterial(pick_write=True) self._plane = pygfx.Mesh(geometry, material) # create resize handler at point (x1, y1) - x1, y1 = self.ranges[[1, 3]] + x1, y1 = self.extent[[1, 3]] self._resize_handler = pygfx.Points( pygfx.Geometry(positions=[[x1, -y1, 0]]), # y is inverted in UnderlayCamera - pygfx.PointsMarkerMaterial(marker="square", size=4, size_space="screen") + pygfx.PointsMarkerMaterial(marker="square", size=4, size_space="screen", pick_write=True) ) self._reset_plane() @@ -132,19 +139,14 @@ def __init__(self, figure, rect: np.ndarray = None, ranges: np.ndarray = None): self._world_object = pygfx.Group() self._world_object.add(self._plane, self._resize_handler) - def _ranges_to_rect(self, ranges) -> np.ndarray: - """convert ranges to rect""" - x0, x1, y0, y1 = ranges + def _extent_to_rect(self, extent) -> np.ndarray: + """convert extent to rect""" + x0, x1, y0, y1 = extent # width and height w = x1 - x0 h = y1 - y0 - if x1 - x0 <= 0: - raise ValueError - if y1 - y0 <= 0: - raise ValueError - x, y, w, h = x0, y0, w, h return np.array([x, y, w, h]) @@ -178,9 +180,9 @@ def _assign_rect(self, rect) -> np.ndarray[int]: elif (rect[2:] > 1).all(): # bbox in already in screen coords coordinates # check that widths, heights are valid if rect[0] + rect[2] > cw: - raise ValueError("invalid value: x + width > 1") + raise ValueError(f"invalid value: x + width > 1: {rect}") if rect[1] + rect[3] > ch: - raise ValueError("invalid value: y + height > 1") + raise ValueError(f"invalid value: y + height > 1: {rect}") self._rect_frac[:] = rect / mult self._rect_screen_space[:] = rect @@ -189,16 +191,63 @@ def _assign_rect(self, rect) -> np.ndarray[int]: raise ValueError(f"Invalid rect: {rect}") @property - def ranges(self) -> np.ndarray: - """ranges, (xmin, xmax, ymin, ymax)""" + def extent(self) -> np.ndarray: + """extent, (xmin, xmax, ymin, ymax)""" # not actually stored, computed when needed return np.asarray([self.rect[0], self.rect[0] + self.rect[2], self.rect[1], self.rect[1] + self.rect[3]]) - @ranges.setter - def ranges(self, ranges: np.ndarray): - rect = self._ranges_to_rect(ranges) + @extent.setter + def extent(self, extent: np.ndarray): + valid, error = self._validate_extent(extent) + + if not valid: + raise ValueError(error) + + rect = self._extent_to_rect(extent) self.rect = rect + def _validate_extent(self, extent: np.ndarray | tuple) -> tuple[bool, None | str]: + x0, x1, y0, y1 = extent + + # width and height + w = x1 - x0 + h = y1 - y0 + + # make sure extent is valid + if (np.asarray(extent) < 0).any(): + return False, f"extent ranges must be non-negative, you have passed: {extent}" + + # check if x1 - x0 <= 0 + if w <= 0: + return False, f"extent x-range is invalid: {extent}" + + # check if y1 - y0 <= 0 + if h <= 0: + return False, f"extent y-range is invalid: {extent}" + + # # calc canvas extent + # cx0, cy0, cw, ch = self._canvas_rect + # cx1 = cx0 + cw + # cy1 = cy0 + ch + # canvas_extent = np.asarray([cx0, cx1, cy0, cy1]) + + # # check that extent is within the bounds of the canvas + # if (x0 > canvas_extent[:2]).any() or (x1 > canvas_extent[:2]).any(): # is x0, x1 beyond canvas x-range + # return False, f"extent x-range is beyond the bounds of the canvas: {extent}" + # + # if (y0 > canvas_extent[2:]).any() or (y1 > canvas_extent[2:]).any(): # is y0, y1 beyond canvas x-range + # return False, f"extent y-range is beyond the bounds of the canvas: {extent}" + + return True, None + + @property + def x_range(self) -> np.ndarray: + return self.extent[:2] + + @property + def y_range(self) -> np.ndarray: + return self.extent[2:] + @property def rect(self) -> np.ndarray[int]: """rect in absolute screen space, (x, y, w, h)""" @@ -233,6 +282,11 @@ def plane(self) -> pygfx.Mesh: """the plane mesh""" return self._plane + @property + def resize_handler(self) -> pygfx.Points: + """resize handler point""" + return self._resize_handler + def _canvas_resized(self, *ev): """triggered when canvas is resized""" # render area, to account for any edge windows that might be present @@ -243,6 +297,28 @@ def _canvas_resized(self, *ev): # set new rect using existing rect_frac since this remains constant regardless of resize self.rect = self._rect_frac + def is_above(self, y0) -> bool: + # our bottom < other top + return self.y_range[1] < y0 + + def is_below(self, y1) -> bool: + # our top > other bottom + return self.y_range[0] > y1 + + def is_left_of(self, x0) -> bool: + # our right_edge < other left_edge + # self.x1 < other.x0 + return self.x_range[1] < x0 + + def is_right_of(self, x1) -> bool: + # self.x0 > other.x1 + return self.x_range[0] > x1 + + def overlaps(self, extent: np.ndarray) -> bool: + """returns whether this subplot would overlap with the other extent""" + x0, x1, y0, y1 = extent + return not any([self.is_above(y0), self.is_below(y1), self.is_left_of(x0), self.is_right_of(x1)]) + def __repr__(self): s = f"{self._rect_frac}\n{self.rect}" @@ -250,12 +326,25 @@ def __repr__(self): class FlexLayoutManager: - def __init__(self, figure, *frames: SubplotFrame): + def __init__(self, figure, frames: SubplotFrame): self.figure = figure - self.figure.renderer.add_event_handler(self._figure_resized, "resize") + # self.figure.renderer.add_event_handler(self._figure_resized, "resize") self._frames = frames + self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array([np.nan, np.nan]) + + self._moving = False + self._resizing = False + self._active_frame: SubplotFrame | None = None + + for frame in self._frames: + frame.plane.add_event_handler(partial(self._action_start, frame, "move"), "pointer_down") + frame.resize_handler.add_event_handler(partial(self._action_start, frame, "resize"), "pointer_down") + + self.figure.renderer.add_event_handler(self._action_iter, "pointer_move") + self.figure.renderer.add_event_handler(self._action_end, "pointer_up") + def _subplot_changed(self): """ Check that this subplot x_range, y_range does not overlap with any other @@ -264,41 +353,90 @@ def _subplot_changed(self): """ pass - def _figure_resized(self, ev): - w, h = ev["width"], ev["height"] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # def _figure_resized(self, ev): + # w, h = ev["width"], ev["height"] + def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: + delta_x, delta_y = delta + if self._resizing: + # subtract only from x1, y1 + new_extent = self._active_frame.extent - np.asarray([0, delta_x, 0, delta_y]) + else: + # moving + new_extent = self._active_frame.extent - np.asarray([delta_x, delta_x, delta_y, delta_y]) + x0, x1, y0, y1 = new_extent + w = x1 - x0 + h = y1 - y0 + # make sure width and height are valid + if w <= 0: # width > 0 + new_extent[:2] = self._active_frame.extent[:2] + + if h <= 0: # height > 0 + new_extent[2:] = self._active_frame.extent[2:] + + # ignore movement if this would cause an overlap + for frame in self._frames: + if frame is self._active_frame: + continue + + if frame.overlaps(new_extent): + # we have an overlap, need to ignore one or more deltas + # ignore x + if not frame.is_left_of(x0) or not frame.is_right_of(x1): + new_extent[:2] = self._active_frame.extent[:2] + + # ignore y + if not frame.is_above(y0) or not frame.is_below(y1): + new_extent[2:] = self._active_frame.extent[2:] + + # make sure all vals are non-negative + if (new_extent[:2] < 0).any(): + # ignore delta_x + new_extent[:2] = self._active_frame.extent[:2] + + if (new_extent[2:] < 0).any(): + # ignore delta_y + new_extent[2:] = self._active_frame.extent[2:] + + # canvas extent + cx0, cy0, cw, ch = self._active_frame._canvas_rect + + # check if new x-range is beyond canvas x-max + if (new_extent[:2] > cx0 + cw).any(): + new_extent[:2] = self._active_frame.extent[:2] + + # check if new y-range is beyond canvas y-max + if (new_extent[2:] > cy0 + ch).any(): + new_extent[2:] = self._active_frame.extent[2:] + + return new_extent + + def _action_start(self, frame: SubplotFrame, action: str, ev): + if ev.button == 1: + if action == "move": + self._moving = True + elif action == "resize": + self._resizing = True + frame.resize_handler.material.color = (1, 0, 0) + else: + raise ValueError + + self._active_frame = frame + self._last_pointer_pos[:] = ev.x, ev.y + + def _action_iter(self, ev): + if not any((self._moving, self._resizing)): + return + + delta_x, delta_y = self._last_pointer_pos - (ev.x, ev.y) + new_extent = self._new_extent_from_delta((delta_x, delta_y)) + self._active_frame.extent = new_extent + self._last_pointer_pos[:] = ev.x, ev.y + + def _action_end(self, ev): + self._moving = False + self._resizing = False + self._active_frame.resize_handler.material.color = (0, 0, 0) + self._last_pointer_pos[:] = np.nan From e48c8526228a335962617f2ae686d10e6b2e1b99 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 1 Mar 2025 04:47:30 -0500 Subject: [PATCH 07/82] handler color, size --- fastplotlib/layouts/_subplot_bbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index f250b5e31..61fd13ee5 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -131,7 +131,7 @@ def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None): x1, y1 = self.extent[[1, 3]] self._resize_handler = pygfx.Points( pygfx.Geometry(positions=[[x1, -y1, 0]]), # y is inverted in UnderlayCamera - pygfx.PointsMarkerMaterial(marker="square", size=4, size_space="screen", pick_write=True) + pygfx.PointsMarkerMaterial(marker="square", size=12, size_space="screen", pick_write=True) ) self._reset_plane() @@ -438,5 +438,5 @@ def _action_iter(self, ev): def _action_end(self, ev): self._moving = False self._resizing = False - self._active_frame.resize_handler.material.color = (0, 0, 0) + self._active_frame.resize_handler.material.color = (1, 1, 1) self._last_pointer_pos[:] = np.nan From eedba34c00971c5deaae0e487c6b27089e66b643 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 1 Mar 2025 04:50:42 -0500 Subject: [PATCH 08/82] cleanup --- fastplotlib/layouts/_subplot_bbox.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index 61fd13ee5..6d0e7cd8a 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -326,7 +326,7 @@ def __repr__(self): class FlexLayoutManager: - def __init__(self, figure, frames: SubplotFrame): + def __init__(self, figure, frames: tuple[SubplotFrame]): self.figure = figure # self.figure.renderer.add_event_handler(self._figure_resized, "resize") @@ -345,17 +345,6 @@ def __init__(self, figure, frames: SubplotFrame): self.figure.renderer.add_event_handler(self._action_iter, "pointer_move") self.figure.renderer.add_event_handler(self._action_end, "pointer_up") - def _subplot_changed(self): - """ - Check that this subplot x_range, y_range does not overlap with any other - - Check that this x_min > all other x_ - """ - pass - - # def _figure_resized(self, ev): - # w, h = ev["width"], ev["height"] - def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: delta_x, delta_y = delta if self._resizing: From a39d383a31ac6b7c7466a6c039e943fcdd749d5f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 2 Mar 2025 01:18:59 -0500 Subject: [PATCH 09/82] resize handler highlight --- fastplotlib/layouts/_subplot_bbox.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index 6d0e7cd8a..024c3c0c2 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -341,6 +341,8 @@ def __init__(self, figure, frames: tuple[SubplotFrame]): for frame in self._frames: frame.plane.add_event_handler(partial(self._action_start, frame, "move"), "pointer_down") frame.resize_handler.add_event_handler(partial(self._action_start, frame, "resize"), "pointer_down") + frame.resize_handler.add_event_handler(self._highlight_resize_handler, "pointer_enter") + frame.resize_handler.add_event_handler(self._unhighlight_resize_handler, "pointer_leave") self.figure.renderer.add_event_handler(self._action_iter, "pointer_move") self.figure.renderer.add_event_handler(self._action_end, "pointer_up") @@ -359,10 +361,11 @@ def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: h = y1 - y0 # make sure width and height are valid - if w <= 0: # width > 0 + # min width, height is 50px + if w <= 50: # width > 0 new_extent[:2] = self._active_frame.extent[:2] - if h <= 0: # height > 0 + if h <= 50: # height > 0 new_extent[2:] = self._active_frame.extent[2:] # ignore movement if this would cause an overlap @@ -429,3 +432,15 @@ def _action_end(self, ev): self._resizing = False self._active_frame.resize_handler.material.color = (1, 1, 1) self._last_pointer_pos[:] = np.nan + + def _highlight_resize_handler(self, ev): + if self._resizing: + return + + ev.target.material.color = (1, 1, 0) + + def _unhighlight_resize_handler(self, ev): + if self._resizing: + return + + ev.target.material.color = (1, 1, 1) From 685830b49cc83f407b8bb8ed256f105c4c9c7308 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 2 Mar 2025 02:11:24 -0500 Subject: [PATCH 10/82] subplot title works, rename to Frame --- fastplotlib/layouts/_subplot_bbox.py | 33 ++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index 024c3c0c2..50df40259 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -3,6 +3,8 @@ import numpy as np import pygfx +from ..graphics import TextGraphic + class UnderlayCamera(pygfx.Camera): """ @@ -83,8 +85,8 @@ class MeshMasks: masks = MeshMasks -class SubplotFrame: - def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None): +class Frame: + def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None, subplot_title: str = None): """ Parameters @@ -95,7 +97,7 @@ def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None): a fractional rect of (0.1, 0.1, 0.5, 0.5) is (10, 10, 50, 100) in absolute screen space extent: (xmin, xmax, ymin, ymax) - range in absolute screen coordinates or fractional screen coordinates + extent of the frame in absolute screen coordinates or fractional screen coordinates """ self.figure = figure @@ -119,6 +121,10 @@ def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None): # convert ranges to rect rect = self._extent_to_rect(extent) + if subplot_title is None: + subplot_title = "" + self._subplot_title = TextGraphic(subplot_title, face_color="black") + # assign the internal state of the rect by parsing the user passed rect self._assign_rect(rect) @@ -127,6 +133,9 @@ def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None): material = pygfx.MeshBasicMaterial(pick_write=True) self._plane = pygfx.Mesh(geometry, material) + # otherwise text isn't visible + self._plane.world.z = 0.5 + # create resize handler at point (x1, y1) x1, y1 = self.extent[[1, 3]] self._resize_handler = pygfx.Points( @@ -137,7 +146,7 @@ def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None): self._reset_plane() self._world_object = pygfx.Group() - self._world_object.add(self._plane, self._resize_handler) + self._world_object.add(self._plane, self._resize_handler, self._subplot_title.world_object) def _extent_to_rect(self, extent) -> np.ndarray: """convert extent to rect""" @@ -277,6 +286,12 @@ def _reset_plane(self): self._resize_handler.geometry.positions.data[0] = [x1, -y1, 0] self._resize_handler.geometry.positions.update_full() + # set subplot title position + x = x0 + (w / 2) + y = y0 + (self.subplot_title.font_size / 2) + self.subplot_title.world_object.world.x = x + self.subplot_title.world_object.world.y = -y + @property def plane(self) -> pygfx.Mesh: """the plane mesh""" @@ -319,6 +334,10 @@ def overlaps(self, extent: np.ndarray) -> bool: x0, x1, y0, y1 = extent return not any([self.is_above(y0), self.is_below(y1), self.is_left_of(x0), self.is_right_of(x1)]) + @property + def subplot_title(self) -> TextGraphic: + return self._subplot_title + def __repr__(self): s = f"{self._rect_frac}\n{self.rect}" @@ -326,7 +345,7 @@ def __repr__(self): class FlexLayoutManager: - def __init__(self, figure, frames: tuple[SubplotFrame]): + def __init__(self, figure, frames: tuple[Frame]): self.figure = figure # self.figure.renderer.add_event_handler(self._figure_resized, "resize") @@ -336,7 +355,7 @@ def __init__(self, figure, frames: tuple[SubplotFrame]): self._moving = False self._resizing = False - self._active_frame: SubplotFrame | None = None + self._active_frame: Frame | None = None for frame in self._frames: frame.plane.add_event_handler(partial(self._action_start, frame, "move"), "pointer_down") @@ -405,7 +424,7 @@ def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: return new_extent - def _action_start(self, frame: SubplotFrame, action: str, ev): + def _action_start(self, frame: Frame, action: str, ev): if ev.button == 1: if action == "move": self._moving = True From 13296289bcff56357e391d094eb5a812e945e1ee Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 2 Mar 2025 02:34:04 -0500 Subject: [PATCH 11/82] start generalizing layout manager --- fastplotlib/layouts/_subplot_bbox.py | 45 ++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index 50df40259..757832188 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -344,13 +344,46 @@ def __repr__(self): return s -class FlexLayoutManager: +class BaseLayoutManager: def __init__(self, figure, frames: tuple[Frame]): - self.figure = figure - # self.figure.renderer.add_event_handler(self._figure_resized, "resize") - + self._figure = figure self._frames = frames + def set_rect(self, subplot, rect: np.ndarray | list | tuple): + raise NotImplementedError + + def set_extent(self, subplot, extent: np.ndarray | list | tuple): + raise NotImplementedError + + def _canvas_resize_handler(self, ev): + pass + + @property + def spacing(self) -> int: + pass + + +class GridLayout(BaseLayoutManager): + def __init__(self, figure, frames: tuple[Frame]): + super().__init__(figure, frames) + + def set_rect(self, subplot, rect: np.ndarray | list | tuple): + raise NotImplementedError("set_rect() not implemented for GridLayout which is an auto layout manager") + + def set_extent(self, subplot, extent: np.ndarray | list | tuple): + raise NotImplementedError("set_extent() not implemented for GridLayout which is an auto layout manager") + + def _fpl_set_subplot_viewport_rect(self): + pass + + def _fpl_set_subplot_dock_viewport_rect(self): + pass + + +class FlexLayout(BaseLayoutManager): + def __init__(self, figure, frames: tuple[Frame]): + super().__init__(figure, frames) + self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array([np.nan, np.nan]) self._moving = False @@ -363,8 +396,8 @@ def __init__(self, figure, frames: tuple[Frame]): frame.resize_handler.add_event_handler(self._highlight_resize_handler, "pointer_enter") frame.resize_handler.add_event_handler(self._unhighlight_resize_handler, "pointer_leave") - self.figure.renderer.add_event_handler(self._action_iter, "pointer_move") - self.figure.renderer.add_event_handler(self._action_end, "pointer_up") + self._figure.renderer.add_event_handler(self._action_iter, "pointer_move") + self._figure.renderer.add_event_handler(self._action_end, "pointer_up") def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: delta_x, delta_y = delta From ce721bd6782b9f05602e93c8c1023417fd5cec44 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 2 Mar 2025 02:45:35 -0500 Subject: [PATCH 12/82] cleaner --- fastplotlib/layouts/_subplot_bbox.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index 757832188..b74634c76 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -386,8 +386,7 @@ def __init__(self, figure, frames: tuple[Frame]): self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array([np.nan, np.nan]) - self._moving = False - self._resizing = False + self._active_action: str | None = None self._active_frame: Frame | None = None for frame in self._frames: @@ -401,7 +400,7 @@ def __init__(self, figure, frames: tuple[Frame]): def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: delta_x, delta_y = delta - if self._resizing: + if self._active_action == "resize": # subtract only from x1, y1 new_extent = self._active_frame.extent - np.asarray([0, delta_x, 0, delta_y]) else: @@ -459,10 +458,8 @@ def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: def _action_start(self, frame: Frame, action: str, ev): if ev.button == 1: - if action == "move": - self._moving = True - elif action == "resize": - self._resizing = True + self._active_action = action + if action == "resize": frame.resize_handler.material.color = (1, 0, 0) else: raise ValueError @@ -471,7 +468,7 @@ def _action_start(self, frame: Frame, action: str, ev): self._last_pointer_pos[:] = ev.x, ev.y def _action_iter(self, ev): - if not any((self._moving, self._resizing)): + if self._active_action is None: return delta_x, delta_y = self._last_pointer_pos - (ev.x, ev.y) @@ -480,19 +477,18 @@ def _action_iter(self, ev): self._last_pointer_pos[:] = ev.x, ev.y def _action_end(self, ev): - self._moving = False - self._resizing = False + self._active_action = None self._active_frame.resize_handler.material.color = (1, 1, 1) self._last_pointer_pos[:] = np.nan def _highlight_resize_handler(self, ev): - if self._resizing: + if self._active_action == "resize": return ev.target.material.color = (1, 1, 0) def _unhighlight_resize_handler(self, ev): - if self._resizing: + if self._active_action == "resize": return ev.target.material.color = (1, 1, 1) From 4d6b395eca9a49f5d918cb37cbc09fdc43ebfb1a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 2 Mar 2025 14:58:34 -0500 Subject: [PATCH 13/82] start rect and extent class for organization --- fastplotlib/layouts/_subplot_bbox.py | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index b74634c76..ec28f5719 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -85,6 +85,41 @@ class MeshMasks: masks = MeshMasks +class Rect: + def __init__(self, x, y, w, h): + pass + + def fractional(self) -> bool: + pass + + @property + def x(self) -> int: + pass + + def to_extent(self): + pass + + def set_from_extent(self, extent): + pass + + +class Extent: + def __init__(self, x0, x1, y0, y1): + pass + + def fractional(self) -> bool: + pass + + def x1(self) -> int: + pass + + def to_rect(self): + pass + + def set_from_rect(self): + pass + + class Frame: def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None, subplot_title: str = None): """ From 5e978187ebed14edd8230a37898d2d0cb28773c5 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 3 Mar 2025 01:26:50 -0500 Subject: [PATCH 14/82] start moving rect logic to a dedicated class --- fastplotlib/layouts/_subplot_bbox.py | 147 +++++++++++++++++++++++---- 1 file changed, 125 insertions(+), 22 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index ec28f5719..b3a8ddc99 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -29,7 +29,8 @@ def _update_projection_matrix(self): The rectangles are viewed using the UnderlayCamera where (0, 0) is the top left corner. We can control the bbox of this rectangle by changing the x and y boundaries of the rectangle. -Note how the y values are negative. +Note how the y values of the plane mesh are negative, this is because of the UnderlayCamera. +We always just keep the positive y value, and make it negative only when setting the plane mesh. Illustration: @@ -86,38 +87,140 @@ class MeshMasks: class Rect: - def __init__(self, x, y, w, h): - pass + def __init__(self, x: float, y: float, w: float, h: float, canvas_rect: tuple): + self._rect_frac = np.zeros(4, dtype=np.float64) + self._rect_screen_space = np.zeros(4, dtype=np.float64) + self._canvas_rect = canvas_rect - def fractional(self) -> bool: - pass + self._set(np.asarray([x, y, w, h])) + + def _set(self, rect): + for val, name in zip(rect, ["x-position", "y-position", "width", "height"]): + if val < 0: + raise ValueError(f"Invalid rect value < 0 for: {name}") + + if (rect[2:] <= 1).all(): # fractional bbox + self._set_from_fract(rect) + + elif (rect[2:] > 1).all(): # bbox in already in screen coords coordinates + self._set_from_screen_space(rect) + + else: + raise ValueError(f"Invalid rect: {rect}") + + def _set_from_fract(self, rect): + _, _, cw, ch = self._canvas_rect + mult = np.array([cw, ch, cw, ch]) + + # check that widths, heights are valid: + if rect[0] + rect[2] > 1: + raise ValueError("invalid fractional value: x + width > 1") + if rect[1] + rect[3] > 1: + raise ValueError("invalid fractional value: y + height > 1") + + # assign values, don't just change the reference + self._rect_frac[:] = rect + self._rect_screen_space[:] = self._rect_frac * mult + + def _set_from_screen_space(self, rect): + _, _, cw, ch = self._canvas_rect + mult = np.array([cw, ch, cw, ch]) + # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 + # check that widths, heights are valid + if rect[0] + rect[2] > cw: + raise ValueError(f"invalid value: x + width > 1: {rect}") + if rect[1] + rect[3] > ch: + raise ValueError(f"invalid value: y + height > 1: {rect}") + + self._rect_frac[:] = rect / mult + self._rect_screen_space[:] = rect @property - def x(self) -> int: - pass + def x(self) -> np.float64: + return self._rect_screen_space[0] - def to_extent(self): - pass + @property + def y(self) -> float: + return self._rect_screen_space[1] - def set_from_extent(self, extent): - pass + @property + def w(self) -> float: + return self._rect_screen_space[2] + @property + def h(self) -> float: + return self._rect_screen_space[3] -class Extent: - def __init__(self, x0, x1, y0, y1): - pass + def _set_canvas_rect(self, rect: tuple): + self._canvas_rect = rect + self._set(self._rect_frac) - def fractional(self) -> bool: - pass + @classmethod + def from_extent(cls, extent, canvas_rect): + rect = cls.extent_to_rect(extent, canvas_rect) - def x1(self) -> int: - pass + @property + def extent(self) -> np.ndarray: + return np.asarray([self.x, self.x + self.w, self.y, self.y + self.h]) - def to_rect(self): - pass + @extent.setter + def extent(self, extent): + """convert extent to rect""" + valid, error = Rect.validate_extent(extent, self._canvas_rect) + if not valid: + raise ValueError(error) - def set_from_rect(self): - pass + rect = Rect.extent_to_rect(extent, canvas_rect=self._canvas_rect) + + self._set(*rect) + + @staticmethod + def extent_to_rect(extent, canvas_rect): + Rect.validate_extent(extent, canvas_rect) + x0, x1, y0, y1 = extent + + # width and height + w = x1 - x0 + h = y1 - y0 + + x, y, w, h = x0, y0, w, h + + return x, y, w, h + + @staticmethod + def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple) -> tuple[bool, None | str]: + x0, x1, y0, y1 = extent + + # width and height + w = x1 - x0 + h = y1 - y0 + + # make sure extent is valid + if (np.asarray(extent) < 0).any(): + return False, f"extent ranges must be non-negative, you have passed: {extent}" + + # check if x1 - x0 <= 0 + if w <= 0: + return False, f"extent x-range is invalid: {extent}" + + # check if y1 - y0 <= 0 + if h <= 0: + return False, f"extent y-range is invalid: {extent}" + + # # calc canvas extent + # cx0, cy0, cw, ch = self._canvas_rect + # cx1 = cx0 + cw + # cy1 = cy0 + ch + # canvas_extent = np.asarray([cx0, cx1, cy0, cy1]) + + # # check that extent is within the bounds of the canvas + # if (x0 > canvas_extent[:2]).any() or (x1 > canvas_extent[:2]).any(): # is x0, x1 beyond canvas x-range + # return False, f"extent x-range is beyond the bounds of the canvas: {extent}" + # + # if (y0 > canvas_extent[2:]).any() or (y1 > canvas_extent[2:]).any(): # is y0, y1 beyond canvas x-range + # return False, f"extent y-range is beyond the bounds of the canvas: {extent}" + + return True, None class Frame: From 06ecc77ef8b031eeab589d5f05e21ee70b2b9fa9 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 4 Mar 2025 23:21:25 -0500 Subject: [PATCH 15/82] organization --- fastplotlib/layouts/_subplot_bbox.py | 288 ++++++++++----------------- 1 file changed, 109 insertions(+), 179 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index b3a8ddc99..278c9f30b 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -86,15 +86,23 @@ class MeshMasks: masks = MeshMasks -class Rect: +class RectManager: def __init__(self, x: float, y: float, w: float, h: float, canvas_rect: tuple): + # initialize rect state arrays + # used to store internal state of the rect in both fractional screen space and absolute screen space + # the purpose of storing the fractional rect is that it remains constant when the canvas resizes self._rect_frac = np.zeros(4, dtype=np.float64) self._rect_screen_space = np.zeros(4, dtype=np.float64) - self._canvas_rect = canvas_rect + self._canvas_rect = np.asarray(canvas_rect) - self._set(np.asarray([x, y, w, h])) + self._set((x, y, w, h)) def _set(self, rect): + """ + Using the passed rect which is either absolute screen space or fractional, + set the internal fractional and absolute screen space rects + """ + rect = np.asarray(rect) for val, name in zip(rect, ["x-position", "y-position", "width", "height"]): if val < 0: raise ValueError(f"Invalid rect value < 0 for: {name}") @@ -109,6 +117,7 @@ def _set(self, rect): raise ValueError(f"Invalid rect: {rect}") def _set_from_fract(self, rect): + """set rect from fractional representation""" _, _, cw, ch = self._canvas_rect mult = np.array([cw, ch, cw, ch]) @@ -123,6 +132,7 @@ def _set_from_fract(self, rect): self._rect_screen_space[:] = self._rect_frac * mult def _set_from_screen_space(self, rect): + """set rect from screen space representation""" _, _, cw, ch = self._canvas_rect mult = np.array([cw, ch, cw, ch]) # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 @@ -140,43 +150,72 @@ def x(self) -> np.float64: return self._rect_screen_space[0] @property - def y(self) -> float: + def y(self) -> np.float64: return self._rect_screen_space[1] @property - def w(self) -> float: + def w(self) -> np.float64: return self._rect_screen_space[2] @property - def h(self) -> float: + def h(self) -> np.float64: return self._rect_screen_space[3] - def _set_canvas_rect(self, rect: tuple): - self._canvas_rect = rect + @property + def rect(self) -> np.ndarray: + return self._rect_screen_space + + @rect.setter + def rect(self, rect: np.ndarray | tuple): + self._set(rect) + + def _fpl_canvas_resized(self, canvas_rect: tuple): + # called by subplot when canvas is resized + self._canvas_rect[:] = canvas_rect + # set new rect using existing rect_frac since this remains constant regardless of resize self._set(self._rect_frac) + @property + def x0(self) -> np.float64: + return self.x + + @property + def x1(self) -> np.float64: + return self.x + self.w + + @property + def y0(self) -> np.float64: + return self.y + + @property + def y1(self) -> np.float64: + return self.y + self.h + @classmethod def from_extent(cls, extent, canvas_rect): rect = cls.extent_to_rect(extent, canvas_rect) + return cls(*rect, canvas_rect) @property def extent(self) -> np.ndarray: - return np.asarray([self.x, self.x + self.w, self.y, self.y + self.h]) + """extent, (xmin, xmax, ymin, ymax)""" + # not actually stored, computed when needed + return np.asarray([self.x0, self.x1, self.y0, self.y1]) @extent.setter def extent(self, extent): """convert extent to rect""" - valid, error = Rect.validate_extent(extent, self._canvas_rect) + valid, error = RectManager.validate_extent(extent, self._canvas_rect) if not valid: raise ValueError(error) - rect = Rect.extent_to_rect(extent, canvas_rect=self._canvas_rect) + rect = RectManager.extent_to_rect(extent, canvas_rect=self._canvas_rect) - self._set(*rect) + self._set(rect) @staticmethod def extent_to_rect(extent, canvas_rect): - Rect.validate_extent(extent, canvas_rect) + RectManager.validate_extent(extent, canvas_rect) x0, x1, y0, y1 = extent # width and height @@ -222,6 +261,11 @@ def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple) -> tuple[boo return True, None + def __repr__(self): + s = f"{self._rect_frac}\n{self.rect}" + + return s + class Frame: def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None, subplot_title: str = None): @@ -238,34 +282,19 @@ def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None, s extent of the frame in absolute screen coordinates or fractional screen coordinates """ self.figure = figure + figure.canvas.add_event_handler(self._canvas_resize_handler, "resize") - # canvas (x, y, w, h) - self._canvas_rect = figure.get_pygfx_render_area() - figure.canvas.add_event_handler(self._canvas_resized, "resize") - - # initialize rect state arrays - # used to store internal state of the rect in both fractional screen space and absolute screen space - # the purpose of storing the fractional rect is that it remains constant when the canvas resizes - self._rect_frac = np.zeros(4, dtype=np.float64) - self._rect_screen_space = np.zeros(4, dtype=np.float64) - - if rect is None: - if extent is None: - raise ValueError("Must provide rect or ranges") - - valid, error = self._validate_extent(extent) - if not valid: - raise ValueError(error) - # convert ranges to rect - rect = self._extent_to_rect(extent) + if rect is not None: + self._rect = RectManager(*rect, figure.get_pygfx_render_area()) + elif extent is not None: + self._rect = RectManager.from_extent(extent, figure.get_pygfx_render_area()) + else: + raise ValueError("Must provide `rect` or `extent`") if subplot_title is None: subplot_title = "" self._subplot_title = TextGraphic(subplot_title, face_color="black") - # assign the internal state of the rect by parsing the user passed rect - self._assign_rect(rect) - # init mesh of size 1 to graphically represent rect geometry = pygfx.plane_geometry(1, 1) material = pygfx.MeshBasicMaterial(pick_write=True) @@ -286,132 +315,32 @@ def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None, s self._world_object = pygfx.Group() self._world_object.add(self._plane, self._resize_handler, self._subplot_title.world_object) - def _extent_to_rect(self, extent) -> np.ndarray: - """convert extent to rect""" - x0, x1, y0, y1 = extent - - # width and height - w = x1 - x0 - h = y1 - y0 - - x, y, w, h = x0, y0, w, h - - return np.array([x, y, w, h]) - - def _assign_rect(self, rect) -> np.ndarray[int]: - """ - Using the passed rect which is either absolute screen space or fractional, - set the internal fractional and absolute screen space rects - """ - for val, name in zip(rect, ["x-position", "y-position", "width", "height"]): - if val < 0: - raise ValueError(f"Invalid rect value < 0 for: {name}") - - rect = np.asarray(rect) - - _, _, cw, ch = self._canvas_rect - mult = np.array([cw, ch, cw, ch]) - - if (rect[2:] <= 1).all(): # fractional bbox - # check that widths, heights are valid: - if rect[0] + rect[2] > 1: - raise ValueError("invalid fractional value: x + width > 1") - if rect[1] + rect[3] > 1: - raise ValueError("invalid fractional value: y + height > 1") - - # assign values, don't just change the reference - self._rect_frac[:] = rect - self._rect_screen_space[:] = self._rect_frac * mult - - # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 - elif (rect[2:] > 1).all(): # bbox in already in screen coords coordinates - # check that widths, heights are valid - if rect[0] + rect[2] > cw: - raise ValueError(f"invalid value: x + width > 1: {rect}") - if rect[1] + rect[3] > ch: - raise ValueError(f"invalid value: y + height > 1: {rect}") - - self._rect_frac[:] = rect / mult - self._rect_screen_space[:] = rect - - else: - raise ValueError(f"Invalid rect: {rect}") - @property def extent(self) -> np.ndarray: """extent, (xmin, xmax, ymin, ymax)""" # not actually stored, computed when needed - return np.asarray([self.rect[0], self.rect[0] + self.rect[2], self.rect[1], self.rect[1] + self.rect[3]]) + return self._rect.extent @extent.setter - def extent(self, extent: np.ndarray): - valid, error = self._validate_extent(extent) - - if not valid: - raise ValueError(error) - - rect = self._extent_to_rect(extent) - self.rect = rect - - def _validate_extent(self, extent: np.ndarray | tuple) -> tuple[bool, None | str]: - x0, x1, y0, y1 = extent - - # width and height - w = x1 - x0 - h = y1 - y0 - - # make sure extent is valid - if (np.asarray(extent) < 0).any(): - return False, f"extent ranges must be non-negative, you have passed: {extent}" - - # check if x1 - x0 <= 0 - if w <= 0: - return False, f"extent x-range is invalid: {extent}" - - # check if y1 - y0 <= 0 - if h <= 0: - return False, f"extent y-range is invalid: {extent}" - - # # calc canvas extent - # cx0, cy0, cw, ch = self._canvas_rect - # cx1 = cx0 + cw - # cy1 = cy0 + ch - # canvas_extent = np.asarray([cx0, cx1, cy0, cy1]) - - # # check that extent is within the bounds of the canvas - # if (x0 > canvas_extent[:2]).any() or (x1 > canvas_extent[:2]).any(): # is x0, x1 beyond canvas x-range - # return False, f"extent x-range is beyond the bounds of the canvas: {extent}" - # - # if (y0 > canvas_extent[2:]).any() or (y1 > canvas_extent[2:]).any(): # is y0, y1 beyond canvas x-range - # return False, f"extent y-range is beyond the bounds of the canvas: {extent}" - - return True, None - - @property - def x_range(self) -> np.ndarray: - return self.extent[:2] - - @property - def y_range(self) -> np.ndarray: - return self.extent[2:] + def extent(self, extent): + self._rect.extent = extent + self._reset_plane() @property def rect(self) -> np.ndarray[int]: """rect in absolute screen space, (x, y, w, h)""" - return self._rect_screen_space + return self._rect.rect @rect.setter def rect(self, rect: np.ndarray): - self._assign_rect(rect) + self._rect.rect = rect self._reset_plane() def _reset_plane(self): """reset the plane mesh using the current rect state""" - x0, y0, w, h = self.rect - - x1 = x0 + w - y1 = y0 + h + x0, x1, y0, y1 = self._rect.extent + w = self._rect.w self._plane.geometry.positions.data[masks.x0] = x0 self._plane.geometry.positions.data[masks.x1] = x1 @@ -431,60 +360,55 @@ def _reset_plane(self): self.subplot_title.world_object.world.y = -y @property - def plane(self) -> pygfx.Mesh: + def _fpl_plane(self) -> pygfx.Mesh: """the plane mesh""" return self._plane @property - def resize_handler(self) -> pygfx.Points: + def _fpl_resize_handler(self) -> pygfx.Points: """resize handler point""" return self._resize_handler - def _canvas_resized(self, *ev): + def _canvas_resize_handler(self, *ev): """triggered when canvas is resized""" # render area, to account for any edge windows that might be present # remember this frame also encapsulates the imgui toolbar which is # part of the subplot so we do not subtract the toolbar height! - self._canvas_rect = self.figure.get_pygfx_render_area() + canvas_rect = self.figure.get_pygfx_render_area() - # set new rect using existing rect_frac since this remains constant regardless of resize - self.rect = self._rect_frac + self._rect._fpl_canvas_resized(canvas_rect) + self._reset_plane() + + @property + def subplot_title(self) -> TextGraphic: + return self._subplot_title def is_above(self, y0) -> bool: # our bottom < other top - return self.y_range[1] < y0 + return self._rect.y1 < y0 def is_below(self, y1) -> bool: # our top > other bottom - return self.y_range[0] > y1 + return self._rect.y0 > y1 def is_left_of(self, x0) -> bool: # our right_edge < other left_edge # self.x1 < other.x0 - return self.x_range[1] < x0 + return self._rect.x1 < x0 def is_right_of(self, x1) -> bool: # self.x0 > other.x1 - return self.x_range[0] > x1 + return self._rect.x0 > x1 def overlaps(self, extent: np.ndarray) -> bool: - """returns whether this subplot would overlap with the other extent""" + """returns whether this subplot overlaps with the given extent""" x0, x1, y0, y1 = extent return not any([self.is_above(y0), self.is_below(y1), self.is_left_of(x0), self.is_right_of(x1)]) - @property - def subplot_title(self) -> TextGraphic: - return self._subplot_title - def __repr__(self): - s = f"{self._rect_frac}\n{self.rect}" - - return s - - -class BaseLayoutManager: - def __init__(self, figure, frames: tuple[Frame]): - self._figure = figure +class BaseLayout: + def __init__(self, renderer: pygfx.WgpuRenderer, frames: tuple[Frame]): + self._renderer = renderer self._frames = frames def set_rect(self, subplot, rect: np.ndarray | list | tuple): @@ -501,7 +425,7 @@ def spacing(self) -> int: pass -class GridLayout(BaseLayoutManager): +class GridLayout(BaseLayout): def __init__(self, figure, frames: tuple[Frame]): super().__init__(figure, frames) @@ -518,23 +442,27 @@ def _fpl_set_subplot_dock_viewport_rect(self): pass -class FlexLayout(BaseLayoutManager): - def __init__(self, figure, frames: tuple[Frame]): - super().__init__(figure, frames) +class FlexLayout(BaseLayout): + def __init__(self, renderer, get_canvas_rect: callable, frames: tuple[Frame]): + super().__init__(renderer, frames) self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array([np.nan, np.nan]) + self._get_canvas_rect = get_canvas_rect + self._active_action: str | None = None self._active_frame: Frame | None = None for frame in self._frames: - frame.plane.add_event_handler(partial(self._action_start, frame, "move"), "pointer_down") - frame.resize_handler.add_event_handler(partial(self._action_start, frame, "resize"), "pointer_down") - frame.resize_handler.add_event_handler(self._highlight_resize_handler, "pointer_enter") - frame.resize_handler.add_event_handler(self._unhighlight_resize_handler, "pointer_leave") + frame._fpl_plane.add_event_handler(partial(self._action_start, frame, "move"), "pointer_down") + frame._fpl_resize_handler.add_event_handler( + partial(self._action_start, frame, "resize"), "pointer_down" + ) + frame._fpl_resize_handler.add_event_handler(self._highlight_resize_handler, "pointer_enter") + frame._fpl_resize_handler.add_event_handler(self._unhighlight_resize_handler, "pointer_leave") - self._figure.renderer.add_event_handler(self._action_iter, "pointer_move") - self._figure.renderer.add_event_handler(self._action_end, "pointer_up") + self._renderer.add_event_handler(self._action_iter, "pointer_move") + self._renderer.add_event_handler(self._action_end, "pointer_up") def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: delta_x, delta_y = delta @@ -582,7 +510,7 @@ def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: new_extent[2:] = self._active_frame.extent[2:] # canvas extent - cx0, cy0, cw, ch = self._active_frame._canvas_rect + cx0, cy0, cw, ch = self._get_canvas_rect() # check if new x-range is beyond canvas x-max if (new_extent[:2] > cx0 + cw).any(): @@ -598,7 +526,9 @@ def _action_start(self, frame: Frame, action: str, ev): if ev.button == 1: self._active_action = action if action == "resize": - frame.resize_handler.material.color = (1, 0, 0) + frame._fpl_resize_handler.material.color = (1, 0, 0) + elif action == "move": + pass else: raise ValueError @@ -616,7 +546,7 @@ def _action_iter(self, ev): def _action_end(self, ev): self._active_action = None - self._active_frame.resize_handler.material.color = (1, 1, 1) + self._active_frame._fpl_resize_handler.material.color = (1, 1, 1) self._last_pointer_pos[:] = np.nan def _highlight_resize_handler(self, ev): From 69b02f3bf243b06b187cbd564ec24bf19cc376a2 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 5 Mar 2025 00:20:14 -0500 Subject: [PATCH 16/82] better extent validation --- fastplotlib/layouts/_subplot_bbox.py | 65 ++++++++++++++++------------ 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py index 278c9f30b..e03c52b9a 100644 --- a/fastplotlib/layouts/_subplot_bbox.py +++ b/fastplotlib/layouts/_subplot_bbox.py @@ -105,7 +105,7 @@ def _set(self, rect): rect = np.asarray(rect) for val, name in zip(rect, ["x-position", "y-position", "width", "height"]): if val < 0: - raise ValueError(f"Invalid rect value < 0 for: {name}") + raise ValueError(f"Invalid rect value < 0: {rect}") if (rect[2:] <= 1).all(): # fractional bbox self._set_from_fract(rect) @@ -147,22 +147,27 @@ def _set_from_screen_space(self, rect): @property def x(self) -> np.float64: + """x position""" return self._rect_screen_space[0] @property def y(self) -> np.float64: + """y position""" return self._rect_screen_space[1] @property def w(self) -> np.float64: + """width""" return self._rect_screen_space[2] @property def h(self) -> np.float64: + """height""" return self._rect_screen_space[3] @property def rect(self) -> np.ndarray: + """rect, (x, y, w, h)""" return self._rect_screen_space @rect.setter @@ -177,22 +182,27 @@ def _fpl_canvas_resized(self, canvas_rect: tuple): @property def x0(self) -> np.float64: + """x0 position""" return self.x @property def x1(self) -> np.float64: + """x1 position""" return self.x + self.w @property def y0(self) -> np.float64: + """y0 position""" return self.y @property def y1(self) -> np.float64: + """y1 position""" return self.y + self.h @classmethod def from_extent(cls, extent, canvas_rect): + """create a RectManager from an extent""" rect = cls.extent_to_rect(extent, canvas_rect) return cls(*rect, canvas_rect) @@ -205,10 +215,6 @@ def extent(self) -> np.ndarray: @extent.setter def extent(self, extent): """convert extent to rect""" - valid, error = RectManager.validate_extent(extent, self._canvas_rect) - if not valid: - raise ValueError(error) - rect = RectManager.extent_to_rect(extent, canvas_rect=self._canvas_rect) self._set(rect) @@ -227,39 +233,44 @@ def extent_to_rect(extent, canvas_rect): return x, y, w, h @staticmethod - def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple) -> tuple[bool, None | str]: + def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): + extent = np.asarray(extent) + cx0, cy0, cw, ch = canvas_rect + + # make sure extent is valid + if (extent < 0).any(): + raise ValueError(f"extent must be non-negative, you have passed: {extent}") + + if extent[1] < 1 or extent[3] < 1: # if x1 < 1, or y1 < 1 + # if fractional rect, convert to full + if not (extent < 1).all(): # if x1 and y1 < 1, then all vals must be < 1 + raise ValueError( + f"if passing a fractional extent, all values must be fractional, you have passed: {extent}") + extent *= np.asarray([cw, cw, ch, ch]) + x0, x1, y0, y1 = extent # width and height w = x1 - x0 h = y1 - y0 - # make sure extent is valid - if (np.asarray(extent) < 0).any(): - return False, f"extent ranges must be non-negative, you have passed: {extent}" - # check if x1 - x0 <= 0 if w <= 0: - return False, f"extent x-range is invalid: {extent}" + raise ValueError(f"extent x-range must be non-negative: {extent}") # check if y1 - y0 <= 0 if h <= 0: - return False, f"extent y-range is invalid: {extent}" - - # # calc canvas extent - # cx0, cy0, cw, ch = self._canvas_rect - # cx1 = cx0 + cw - # cy1 = cy0 + ch - # canvas_extent = np.asarray([cx0, cx1, cy0, cy1]) - - # # check that extent is within the bounds of the canvas - # if (x0 > canvas_extent[:2]).any() or (x1 > canvas_extent[:2]).any(): # is x0, x1 beyond canvas x-range - # return False, f"extent x-range is beyond the bounds of the canvas: {extent}" - # - # if (y0 > canvas_extent[2:]).any() or (y1 > canvas_extent[2:]).any(): # is y0, y1 beyond canvas x-range - # return False, f"extent y-range is beyond the bounds of the canvas: {extent}" - - return True, None + raise ValueError(f"extent y-range must be non-negative: {extent}") + + # calc canvas extent + cx1 = cx0 + cw + cy1 = cy0 + ch + canvas_extent = np.asarray([cx0, cx1, cy0, cy1]) + + if x0 < cx0 or x1 < cx0 or x0 > cx1 or x1 > cx1: + raise ValueError(f"extent: {extent} x-range is beyond the bounds of the canvas: {canvas_extent}") + if y0 < cy0 or y1 < cy0 or y0 > cy1 or y1 > cy1: + raise ValueError(f"extent: {extent} y-range is beyond the bounds of the canvas: {canvas_extent}") def __repr__(self): s = f"{self._rect_frac}\n{self.rect}" From 66b78aecf30d024c78af12572b9d11a3fc4ddd51 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Mar 2025 02:00:27 -0500 Subject: [PATCH 17/82] progress --- fastplotlib/layouts/_engine.py | 349 ++++++++++++++++ fastplotlib/layouts/_figure.py | 144 ++++--- fastplotlib/layouts/_rect.py | 193 +++++++++ fastplotlib/layouts/_subplot.py | 218 ++++++++-- fastplotlib/layouts/_subplot_bbox.py | 573 --------------------------- 5 files changed, 822 insertions(+), 655 deletions(-) create mode 100644 fastplotlib/layouts/_engine.py create mode 100644 fastplotlib/layouts/_rect.py delete mode 100644 fastplotlib/layouts/_subplot_bbox.py diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py new file mode 100644 index 000000000..bee94ae00 --- /dev/null +++ b/fastplotlib/layouts/_engine.py @@ -0,0 +1,349 @@ +from functools import partial + +import numpy as np +import pygfx + +from ._subplot import Subplot + +# from ..graphics import TextGraphic + + +class UnderlayCamera(pygfx.Camera): + """ + Same as pygfx.ScreenCoordsCamera but y-axis is inverted. + + So top right is (0, 0). This is easier to manage because we + often resize using the bottom right corner. + """ + + def _update_projection_matrix(self): + width, height = self._view_size + sx, sy, sz = 2 / width, 2 / height, 1 + dx, dy, dz = -1, 1, 0 # pygfx is -1, -1, 0 + m = sx, 0, 0, dx, 0, sy, 0, dy, 0, 0, sz, dz, 0, 0, 0, 1 + proj_matrix = np.array(m, dtype=float).reshape(4, 4) + proj_matrix.flags.writeable = False + return proj_matrix + + +# class Frame: +# def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None, subplot_title: str = None): +# """ +# +# Parameters +# ---------- +# figure +# rect: (x, y, w, h) +# in absolute screen space or fractional screen space, example if the canvas w, h is (100, 200) +# a fractional rect of (0.1, 0.1, 0.5, 0.5) is (10, 10, 50, 100) in absolute screen space +# +# extent: (xmin, xmax, ymin, ymax) +# extent of the frame in absolute screen coordinates or fractional screen coordinates +# """ +# self.figure = figure +# figure.canvas.add_event_handler(self._canvas_resize_handler, "resize") +# +# if rect is not None: +# self._rect = RectManager(*rect, figure.get_pygfx_render_area()) +# elif extent is not None: +# self._rect = RectManager.from_extent(extent, figure.get_pygfx_render_area()) +# else: +# raise ValueError("Must provide `rect` or `extent`") +# +# if subplot_title is None: +# subplot_title = "" +# self._subplot_title = TextGraphic(subplot_title, face_color="black") +# +# # init mesh of size 1 to graphically represent rect +# geometry = pygfx.plane_geometry(1, 1) +# material = pygfx.MeshBasicMaterial(pick_write=True) +# self._plane = pygfx.Mesh(geometry, material) +# +# # otherwise text isn't visible +# self._plane.world.z = 0.5 +# +# # create resize handler at point (x1, y1) +# x1, y1 = self.extent[[1, 3]] +# self._resize_handler = pygfx.Points( +# pygfx.Geometry(positions=[[x1, -y1, 0]]), # y is inverted in UnderlayCamera +# pygfx.PointsMarkerMaterial(marker="square", size=12, size_space="screen", pick_write=True) +# ) +# +# self._reset_plane() +# +# self._world_object = pygfx.Group() +# self._world_object.add(self._plane, self._resize_handler, self._subplot_title.world_object) +# +# @property +# def extent(self) -> np.ndarray: +# """extent, (xmin, xmax, ymin, ymax)""" +# # not actually stored, computed when needed +# return self._rect.extent +# +# @extent.setter +# def extent(self, extent): +# self._rect.extent = extent +# self._reset_plane() +# +# @property +# def rect(self) -> np.ndarray[int]: +# """rect in absolute screen space, (x, y, w, h)""" +# return self._rect.rect +# +# @rect.setter +# def rect(self, rect: np.ndarray): +# self._rect.rect = rect +# self._reset_plane() +# +# def _reset_plane(self): +# """reset the plane mesh using the current rect state""" +# +# x0, x1, y0, y1 = self._rect.extent +# w = self._rect.w +# +# self._plane.geometry.positions.data[masks.x0] = x0 +# self._plane.geometry.positions.data[masks.x1] = x1 +# self._plane.geometry.positions.data[masks.y0] = -y0 # negative y because UnderlayCamera y is inverted +# self._plane.geometry.positions.data[masks.y1] = -y1 +# +# self._plane.geometry.positions.update_full() +# +# # note the negative y because UnderlayCamera y is inverted +# self._resize_handler.geometry.positions.data[0] = [x1, -y1, 0] +# self._resize_handler.geometry.positions.update_full() +# +# # set subplot title position +# x = x0 + (w / 2) +# y = y0 + (self.subplot_title.font_size / 2) +# self.subplot_title.world_object.world.x = x +# self.subplot_title.world_object.world.y = -y +# +# @property +# def _fpl_plane(self) -> pygfx.Mesh: +# """the plane mesh""" +# return self._plane +# +# @property +# def _fpl_resize_handler(self) -> pygfx.Points: +# """resize handler point""" +# return self._resize_handler +# +# def _canvas_resize_handler(self, *ev): +# """triggered when canvas is resized""" +# # render area, to account for any edge windows that might be present +# # remember this frame also encapsulates the imgui toolbar which is +# # part of the subplot so we do not subtract the toolbar height! +# canvas_rect = self.figure.get_pygfx_render_area() +# +# self._rect._fpl_canvas_resized(canvas_rect) +# self._reset_plane() +# +# @property +# def subplot_title(self) -> TextGraphic: +# return self._subplot_title +# +# def is_above(self, y0) -> bool: +# # our bottom < other top +# return self._rect.y1 < y0 +# +# def is_below(self, y1) -> bool: +# # our top > other bottom +# return self._rect.y0 > y1 +# +# def is_left_of(self, x0) -> bool: +# # our right_edge < other left_edge +# # self.x1 < other.x0 +# return self._rect.x1 < x0 +# +# def is_right_of(self, x1) -> bool: +# # self.x0 > other.x1 +# return self._rect.x0 > x1 +# +# def overlaps(self, extent: np.ndarray) -> bool: +# """returns whether this subplot overlaps with the given extent""" +# x0, x1, y0, y1 = extent +# return not any([self.is_above(y0), self.is_below(y1), self.is_left_of(x0), self.is_right_of(x1)]) + + +class BaseLayout: + def __init__(self, renderer: pygfx.WgpuRenderer, subplots: list[Subplot]): + self._renderer = renderer + self._subplots = subplots + + def set_rect(self, subplot, rect: np.ndarray | list | tuple): + raise NotImplementedError + + def set_extent(self, subplot, extent: np.ndarray | list | tuple): + raise NotImplementedError + + def _canvas_resize_handler(self, ev): + pass + + @property + def spacing(self) -> int: + pass + + def __len__(self): + return len(self._subplots) + + +class FlexLayout(BaseLayout): + def __init__(self, renderer, get_canvas_rect: callable, subplots: list[Subplot]): + super().__init__(renderer, subplots) + + self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array([np.nan, np.nan]) + + self._get_canvas_rect = get_canvas_rect + + self._active_action: str | None = None + self._active_subplot: Subplot | None = None + + for frame in self._subplots: + frame._fpl_plane.add_event_handler(partial(self._action_start, frame, "move"), "pointer_down") + frame._fpl_resize_handler.add_event_handler( + partial(self._action_start, frame, "resize"), "pointer_down" + ) + frame._fpl_resize_handler.add_event_handler(self._highlight_resize_handler, "pointer_enter") + frame._fpl_resize_handler.add_event_handler(self._unhighlight_resize_handler, "pointer_leave") + + self._renderer.add_event_handler(self._action_iter, "pointer_move") + self._renderer.add_event_handler(self._action_end, "pointer_up") + + def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: + delta_x, delta_y = delta + if self._active_action == "resize": + # subtract only from x1, y1 + new_extent = self._active_subplot.extent - np.asarray([0, delta_x, 0, delta_y]) + else: + # moving + new_extent = self._active_subplot.extent - np.asarray([delta_x, delta_x, delta_y, delta_y]) + + x0, x1, y0, y1 = new_extent + w = x1 - x0 + h = y1 - y0 + + # make sure width and height are valid + # min width, height is 50px + if w <= 50: # width > 0 + new_extent[:2] = self._active_subplot.extent[:2] + + if h <= 50: # height > 0 + new_extent[2:] = self._active_subplot.extent[2:] + + # ignore movement if this would cause an overlap + for frame in self._subplots: + if frame is self._active_subplot: + continue + + if frame.overlaps(new_extent): + # we have an overlap, need to ignore one or more deltas + # ignore x + if not frame.is_left_of(x0) or not frame.is_right_of(x1): + new_extent[:2] = self._active_subplot.extent[:2] + + # ignore y + if not frame.is_above(y0) or not frame.is_below(y1): + new_extent[2:] = self._active_subplot.extent[2:] + + # make sure all vals are non-negative + if (new_extent[:2] < 0).any(): + # ignore delta_x + new_extent[:2] = self._active_subplot.extent[:2] + + if (new_extent[2:] < 0).any(): + # ignore delta_y + new_extent[2:] = self._active_subplot.extent[2:] + + # canvas extent + cx0, cy0, cw, ch = self._get_canvas_rect() + + # check if new x-range is beyond canvas x-max + if (new_extent[:2] > cx0 + cw).any(): + new_extent[:2] = self._active_subplot.extent[:2] + + # check if new y-range is beyond canvas y-max + if (new_extent[2:] > cy0 + ch).any(): + new_extent[2:] = self._active_subplot.extent[2:] + + return new_extent + + def _action_start(self, subplot: Subplot, action: str, ev): + if ev.button == 1: + self._active_action = action + if action == "resize": + subplot._fpl_resize_handler.material.color = (1, 0, 0) + elif action == "move": + pass + else: + raise ValueError + + self._active_subplot = subplot + self._last_pointer_pos[:] = ev.x, ev.y + + def _action_iter(self, ev): + if self._active_action is None: + return + + delta_x, delta_y = self._last_pointer_pos - (ev.x, ev.y) + new_extent = self._new_extent_from_delta((delta_x, delta_y)) + self._active_subplot.extent = new_extent + self._last_pointer_pos[:] = ev.x, ev.y + + def _action_end(self, ev): + self._active_action = None + self._active_subplot._fpl_resize_handler.material.color = (1, 1, 1) + self._last_pointer_pos[:] = np.nan + + def _highlight_resize_handler(self, ev): + if self._active_action == "resize": + return + + ev.target.material.color = (1, 1, 0) + + def _unhighlight_resize_handler(self, ev): + if self._active_action == "resize": + return + + ev.target.material.color = (1, 1, 1) + + def add_subplot(self): + pass + + def remove_subplot(self): + pass + + +class GridLayout(FlexLayout): + def __init__(self, renderer, get_canvas_rect: callable, subplots: list[Subplot]): + super().__init__(renderer, subplots) + + # {Subplot: (row_ix, col_ix)}, dict mapping subplots to their row and col index in the grid layout + self._subplot_grid_position: dict[Subplot, tuple[int, int]] + + @property + def shape(self) -> tuple[int, int]: + pass + + def set_rect(self, subplot, rect: np.ndarray | list | tuple): + raise NotImplementedError("set_rect() not implemented for GridLayout which is an auto layout manager") + + def set_extent(self, subplot, extent: np.ndarray | list | tuple): + raise NotImplementedError("set_extent() not implemented for GridLayout which is an auto layout manager") + + def _fpl_set_subplot_viewport_rect(self): + pass + + def _fpl_set_subplot_dock_viewport_rect(self): + pass + + def add_row(self): + pass + + def add_column(self): + pass + + def remove_row(self): + pass + + def remove_column(self): + pass diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index e09005a4c..a1632345d 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -14,6 +14,7 @@ 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 ._engine import GridLayout, FlexLayout, UnderlayCamera from .. import ImageGraphic @@ -24,7 +25,9 @@ class Figure: def __init__( self, - shape: list[tuple[int, int, int, int]] | tuple[int, int] = (1, 1), + shape: tuple[int, int] = (1, 1), + rects: list[tuple | np.ndarray] = None, + extents: list[tuple | np.ndarray] = None, cameras: ( Literal["2d", "3d"] | Iterable[Iterable[Literal["2d", "3d"]]] @@ -97,35 +100,42 @@ def __init__( subplot names """ - if isinstance(shape, list): - raise NotImplementedError("bounding boxes for shape not yet implemented") - if not all(isinstance(v, (tuple, list)) for v in shape): + if rects is not None: + if not all(isinstance(v, (np.ndarray, tuple, list)) for v in shape): raise TypeError( - "shape argument must be a list of bounding boxes or a tuple[n_rows, n_cols]" + f"rects must a list of arrays, tuples, or lists of rects (x, y, w, h), you have passed: {rects}" ) - for item in shape: - if not all(isinstance(v, (int, np.integer)) for v in item): - raise TypeError( - "shape argument must be a list of bounding boxes or a tuple[n_rows, n_cols]" - ) - # constant that sets the Figure to be in "rect" mode - self._mode: str = "rect" + n_subplots = len(rects) + layout_mode = "rect" - elif isinstance(shape, tuple): + elif extents is not None: + if not all(isinstance(v, (np.ndarray, tuple, list)) for v in shape): + raise TypeError( + f"extents must a list of arrays, tuples, or lists of extents (xmin, xmax, ymin, ymax), " + f"you have passed: {extents}" + ) + n_subplots = len(extents) + layout_mode = "extent" + + else: if not all(isinstance(v, (int, np.integer)) for v in shape): raise TypeError( "shape argument must be a list of bounding boxes or a tuple[n_rows, n_cols]" ) - # constant that sets the Figure to be in "grid" mode - self._mode: str = "grid" - + n_subplots = shape[0] * shape[1] # shape is [n_subplots, row_col_index] self._subplot_grid_positions: dict[Subplot, tuple[int, int]] = dict() + layout_mode = "grid" - else: - raise TypeError( - "shape argument must be a list of bounding boxes or a tuple[n_rows, n_cols]" - ) + # create fractional extents from the grid + x_mins = np.arange(0, 1, (1 / shape[0])) + x_maxs = x_mins + 1 / shape[0] + y_mins = np.arange(0, 1, (1 / shape[1])) + y_maxs = y_mins + 1 / shape[1] + + extents = np.column_stack([x_mins, x_maxs, y_mins, y_maxs]) + # empty rects + rects = [None] * n_subplots self._shape = shape @@ -323,34 +333,55 @@ def __init__( self._canvas = canvas self._renderer = renderer - if self.mode == "grid": - nrows, ncols = self.shape - - self._subplots: np.ndarray[Subplot] = np.ndarray( - shape=(nrows, ncols), dtype=object + if layout_mode == "grid": + n_rows, n_cols = self._shape + grid_index_iterator = list(product(range(n_rows), range(n_cols))) + self._subplots: np.ndarray[Subplot] = np.empty( + shape=self._shape, dtype=object ) - for i, (row_ix, col_ix) in enumerate(product(range(nrows), range(ncols))): - camera = subplot_cameras[i] - controller = subplot_controllers[i] + else: + self._subplots = np.ndarray[Subplot] = np.empty(shape=n_subplots, dtype=object) - if subplot_names is not None: - name = subplot_names[i] - else: - name = None - - subplot = Subplot( - parent=self, - camera=camera, - controller=controller, - canvas=canvas, - renderer=renderer, - name=name, - ) + for i in range(n_subplots): + camera = subplot_cameras[i] + controller = subplot_controllers[i] - self._subplots[row_ix, col_ix] = subplot + if subplot_names is not None: + name = subplot_names[i] + else: + name = None + + subplot = Subplot( + parent=self, + camera=camera, + controller=controller, + canvas=canvas, + renderer=renderer, + name=name, + rect=rects[i], + extent=extents[i], # figure created extents for grid layout + ) + if layout_mode == "grid": + row_ix, col_ix = grid_index_iterator[i] + self._subplots[row_ix, col_ix] = subplot self._subplot_grid_positions[subplot] = (row_ix, col_ix) + else: + self._subplots[i] = subplot + + if layout_mode == "grid": + self._layout = GridLayout(self.renderer, self.get_pygfx_render_area, subplots=self._subplots) + + elif layout_mode == "rect" or layout_mode == "extent": + self._layout = FlexLayout(self.renderer, self.get_pygfx_render_area, subplots=self._subplots) + + self._underlay_camera = UnderlayCamera() + + self._underlay_scene = pygfx.Scene() + + for subplot in self._subplots: + self._underlay_scene.add(subplot._world_object) self._animate_funcs_pre: list[callable] = list() self._animate_funcs_post: list[callable] = list() @@ -366,17 +397,15 @@ def __init__( @property def shape(self) -> list[tuple[int, int, int, int]] | tuple[int, int]: """[n_rows, n_cols]""" - return self._shape + if isinstance(self.layout, GridLayout): + return self.layout.shape @property - def mode(self) -> str: + def layout(self) -> FlexLayout | GridLayout: """ - one of 'grid' or 'rect' - - Used by Figure to determine certain aspects, such as how to calculate - rects and shapes of properties for cameras, controllers, and subplots arrays + Layout engine """ - return self._mode + return self._layout @property def spacing(self) -> int: @@ -407,7 +436,7 @@ def controllers(self) -> np.ndarray[pygfx.Controller]: """controllers, read-only array, access individual subplots to change a controller""" controllers = np.asarray([subplot.controller for subplot in self], dtype=object) - if self.mode == "grid": + if isinstance(self.layout, GridLayout): controllers = controllers.reshape(self.shape) controllers.flags.writeable = False @@ -418,7 +447,7 @@ def cameras(self) -> np.ndarray[pygfx.Camera]: """cameras, read-only array, access individual subplots to change a camera""" cameras = np.asarray([subplot.camera for subplot in self], dtype=object) - if self.mode == "grid": + if isinstance(self.layout, GridLayout): cameras = cameras.reshape(self.shape) cameras.flags.writeable = False @@ -429,7 +458,7 @@ def names(self) -> np.ndarray[str]: """subplot names, read-only array, access individual subplots to change a name""" names = np.asarray([subplot.name for subplot in self]) - if self.mode == "grid": + if isinstance(self.layout, GridLayout): names = names.reshape(self.shape) names.flags.writeable = False @@ -442,12 +471,15 @@ def __getitem__(self, index: str | int | tuple[int, int]) -> Subplot: return subplot raise IndexError(f"no subplot with given name: {index}") - if self.mode == "grid": + if isinstance(self.layout, GridLayout): return self._subplots[index[0], index[1]] return self._subplots[index] def _render(self, draw=True): + # draw the underlay planes + self.renderer.render(self._underlay_scene, self._underlay_camera) + # call the animation functions before render self._call_animate_functions(self._animate_funcs_pre) for subplot in self: @@ -859,6 +891,7 @@ def _fpl_set_subplot_dock_viewport_rect(self, subplot, position): def _set_viewport_rects(self, *ev): """set the viewport rects for all subplots, *ev argument is not used, exists because of renderer resize event""" + for subplot in self: self._fpl_set_subplot_viewport_rect(subplot) for dock_pos in subplot.docks.keys(): @@ -890,10 +923,7 @@ def __next__(self) -> Subplot: def __len__(self): """number of subplots""" - if isinstance(self._shape, tuple): - return self.shape[0] * self.shape[1] - if isinstance(self._shape, list): - return len(self._shape) + return len(self._layout) def __str__(self): return f"{self.__class__.__name__} @ {hex(id(self))}" diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py new file mode 100644 index 000000000..d8ea480bf --- /dev/null +++ b/fastplotlib/layouts/_rect.py @@ -0,0 +1,193 @@ +import numpy as np + + +class RectManager: + def __init__(self, x: float, y: float, w: float, h: float, canvas_rect: tuple): + # initialize rect state arrays + # used to store internal state of the rect in both fractional screen space and absolute screen space + # the purpose of storing the fractional rect is that it remains constant when the canvas resizes + self._rect_frac = np.zeros(4, dtype=np.float64) + self._rect_screen_space = np.zeros(4, dtype=np.float64) + self._canvas_rect = np.asarray(canvas_rect) + + self._set((x, y, w, h)) + + def _set(self, rect): + """ + Using the passed rect which is either absolute screen space or fractional, + set the internal fractional and absolute screen space rects + """ + rect = np.asarray(rect) + for val, name in zip(rect, ["x-position", "y-position", "width", "height"]): + if val < 0: + raise ValueError(f"Invalid rect value < 0: {rect}") + + if (rect[2:] <= 1).all(): # fractional bbox + self._set_from_fract(rect) + + elif (rect[2:] > 1).all(): # bbox in already in screen coords coordinates + self._set_from_screen_space(rect) + + else: + raise ValueError(f"Invalid rect: {rect}") + + def _set_from_fract(self, rect): + """set rect from fractional representation""" + _, _, cw, ch = self._canvas_rect + mult = np.array([cw, ch, cw, ch]) + + # check that widths, heights are valid: + if rect[0] + rect[2] > 1: + raise ValueError("invalid fractional value: x + width > 1") + if rect[1] + rect[3] > 1: + raise ValueError("invalid fractional value: y + height > 1") + + # assign values, don't just change the reference + self._rect_frac[:] = rect + self._rect_screen_space[:] = self._rect_frac * mult + + def _set_from_screen_space(self, rect): + """set rect from screen space representation""" + _, _, cw, ch = self._canvas_rect + mult = np.array([cw, ch, cw, ch]) + # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 + # check that widths, heights are valid + if rect[0] + rect[2] > cw: + raise ValueError(f"invalid value: x + width > 1: {rect}") + if rect[1] + rect[3] > ch: + raise ValueError(f"invalid value: y + height > 1: {rect}") + + self._rect_frac[:] = rect / mult + self._rect_screen_space[:] = rect + + @property + def x(self) -> np.float64: + """x position""" + return self._rect_screen_space[0] + + @property + def y(self) -> np.float64: + """y position""" + return self._rect_screen_space[1] + + @property + def w(self) -> np.float64: + """width""" + return self._rect_screen_space[2] + + @property + def h(self) -> np.float64: + """height""" + return self._rect_screen_space[3] + + @property + def rect(self) -> np.ndarray: + """rect, (x, y, w, h)""" + return self._rect_screen_space + + @rect.setter + def rect(self, rect: np.ndarray | tuple): + self._set(rect) + + def _fpl_canvas_resized(self, canvas_rect: tuple): + # called by subplot when canvas is resized + self._canvas_rect[:] = canvas_rect + # set new rect using existing rect_frac since this remains constant regardless of resize + self._set(self._rect_frac) + + @property + def x0(self) -> np.float64: + """x0 position""" + return self.x + + @property + def x1(self) -> np.float64: + """x1 position""" + return self.x + self.w + + @property + def y0(self) -> np.float64: + """y0 position""" + return self.y + + @property + def y1(self) -> np.float64: + """y1 position""" + return self.y + self.h + + @classmethod + def from_extent(cls, extent, canvas_rect): + """create a RectManager from an extent""" + rect = cls.extent_to_rect(extent, canvas_rect) + return cls(*rect, canvas_rect) + + @property + def extent(self) -> np.ndarray: + """extent, (xmin, xmax, ymin, ymax)""" + # not actually stored, computed when needed + return np.asarray([self.x0, self.x1, self.y0, self.y1]) + + @extent.setter + def extent(self, extent): + """convert extent to rect""" + rect = RectManager.extent_to_rect(extent, canvas_rect=self._canvas_rect) + + self._set(rect) + + @staticmethod + def extent_to_rect(extent, canvas_rect): + RectManager.validate_extent(extent, canvas_rect) + x0, x1, y0, y1 = extent + + # width and height + w = x1 - x0 + h = y1 - y0 + + x, y, w, h = x0, y0, w, h + + return x, y, w, h + + @staticmethod + def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): + extent = np.asarray(extent) + cx0, cy0, cw, ch = canvas_rect + + # make sure extent is valid + if (extent < 0).any(): + raise ValueError(f"extent must be non-negative, you have passed: {extent}") + + if extent[1] < 1 or extent[3] < 1: # if x1 < 1, or y1 < 1 + # if fractional rect, convert to full + if not (extent < 1).all(): # if x1 and y1 < 1, then all vals must be < 1 + raise ValueError( + f"if passing a fractional extent, all values must be fractional, you have passed: {extent}") + extent *= np.asarray([cw, cw, ch, ch]) + + x0, x1, y0, y1 = extent + + # width and height + w = x1 - x0 + h = y1 - y0 + + # check if x1 - x0 <= 0 + if w <= 0: + raise ValueError(f"extent x-range must be non-negative: {extent}") + + # check if y1 - y0 <= 0 + if h <= 0: + raise ValueError(f"extent y-range must be non-negative: {extent}") + + # calc canvas extent + cx1 = cx0 + cw + cy1 = cy0 + ch + canvas_extent = np.asarray([cx0, cx1, cy0, cy1]) + + if x0 < cx0 or x1 < cx0 or x0 > cx1 or x1 > cx1: + raise ValueError(f"extent: {extent} x-range is beyond the bounds of the canvas: {canvas_extent}") + if y0 < cy0 or y1 < cy0 or y0 > cy1 or y1 > cy1: + raise ValueError(f"extent: {extent} y-range is beyond the bounds of the canvas: {canvas_extent}") + + def __repr__(self): + s = f"{self._rect_frac}\n{self.rect}" + + return s diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index a97e89b0d..6bdccdb57 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -1,9 +1,11 @@ from typing import Literal, Union -import pygfx +import numpy as np +import pygfx from rendercanvas import BaseRenderCanvas +from ._rect import RectManager from ..graphics import TextGraphic from ._utils import create_camera, create_controller from ._plot_area import PlotArea @@ -11,6 +13,68 @@ from ..graphics._axes import Axes +""" +Each subplot is defined by a 2D plane mesh, a rectangle. +The rectangles are viewed using the UnderlayCamera where (0, 0) is the top left corner. +We can control the bbox of this rectangle by changing the x and y boundaries of the rectangle. + +Note how the y values of the plane mesh are negative, this is because of the UnderlayCamera. +We always just keep the positive y value, and make it negative only when setting the plane mesh. + +Illustration: + +(0, 0) --------------------------------------------------- +---------------------------------------------------------- +---------------------------------------------------------- +--------------(x0, -y0) --------------- (x1, -y0) -------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||rectangle|||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +--------------(x0, -y1) --------------- (x1, -y1)--------- +---------------------------------------------------------- +------------------------------------------- (canvas_width, canvas_height) + +""" + + +class MeshMasks: + """Used set the x1, x1, y0, y1 positions of the mesh""" + x0 = np.array([ + [False, False, False], + [True, False, False], + [False, False, False], + [True, False, False], + ]) + + x1 = np.array([ + [True, False, False], + [False, False, False], + [True, False, False], + [False, False, False], + ]) + + y0 = np.array([ + [False, True, False], + [False, True, False], + [False, False, False], + [False, False, False], + ]) + + y1 = np.array([ + [False, False, False], + [False, False, False], + [False, True, False], + [False, True, False], + ]) + + +masks = MeshMasks + + class Subplot(PlotArea, GraphicMethodsMixin): def __init__( self, @@ -18,6 +82,8 @@ def __init__( camera: Literal["2d", "3d"] | pygfx.PerspectiveCamera, controller: pygfx.Controller, canvas: BaseRenderCanvas | pygfx.Texture, + rect: np.ndarray = None, + extent: np.ndarray = None, renderer: pygfx.WgpuRenderer = None, name: str = None, ): @@ -64,8 +130,6 @@ def __init__( self._docks = dict() - self._title_graphic: TextGraphic = None - self._toolbar = True super(Subplot, self).__init__( @@ -84,12 +148,42 @@ def __init__( self.docks[pos] = dv self.children.append(dv) - if self.name is not None: - self.set_title(self.name) - self._axes = Axes(self) self.scene.add(self.axes.world_object) + if rect is not None: + self._rect = RectManager(*rect, self.get_figure().get_pygfx_render_area()) + elif extent is not None: + self._rect = RectManager.from_extent(extent, self.get_figure().get_pygfx_render_area()) + else: + raise ValueError("Must provide `rect` or `extent`") + + if name is None: + title_text = "" + else: + title_text = name + self._title_graphic = TextGraphic(title_text, face_color="black") + + # init mesh of size 1 to graphically represent rect + geometry = pygfx.plane_geometry(1, 1) + material = pygfx.MeshBasicMaterial(pick_write=True) + self._plane = pygfx.Mesh(geometry, material) + + # otherwise text isn't visible + self._plane.world.z = 0.5 + + # create resize handler at point (x1, y1) + x1, y1 = self.extent[[1, 3]] + self._resize_handler = pygfx.Points( + pygfx.Geometry(positions=[[x1, -y1, 0]]), # y is inverted in UnderlayCamera + pygfx.PointsMarkerMaterial(marker="square", size=12, size_space="screen", pick_write=True) + ) + + self._reset_plane() + + self._world_object = pygfx.Group() + self._world_object.add(self._plane, self._resize_handler, self._title_graphic.world_object) + @property def axes(self) -> Axes: return self._axes @@ -141,31 +235,105 @@ def _render(self): self.axes.update_using_camera() super()._render() - def set_title(self, text: str): - """Sets the plot title, stored as a ``TextGraphic`` in the "top" dock area""" - if text is None: - return + @property + def title(self) -> TextGraphic: + """subplot title""" + return self._title_graphic + @title.setter + def title(self, text: str): text = str(text) - if self._title_graphic is not None: - self._title_graphic.text = text - else: - tg = TextGraphic(text=text, font_size=18) - self._title_graphic = tg + self._title_graphic.text = text - self.docks["top"].size = 35 - self.docks["top"].add_graphic(tg) + @property + def extent(self) -> np.ndarray: + """extent, (xmin, xmax, ymin, ymax)""" + # not actually stored, computed when needed + return self._rect.extent + + @extent.setter + def extent(self, extent): + self._rect.extent = extent + self._reset_plane() + + @property + def rect(self) -> np.ndarray[int]: + """rect in absolute screen space, (x, y, w, h)""" + return self._rect.rect + + @rect.setter + def rect(self, rect: np.ndarray): + self._rect.rect = rect + self._reset_plane() + + def _reset_plane(self): + """reset the plane mesh using the current rect state""" + + x0, x1, y0, y1 = self._rect.extent + w = self._rect.w + + self._plane.geometry.positions.data[masks.x0] = x0 + self._plane.geometry.positions.data[masks.x1] = x1 + self._plane.geometry.positions.data[masks.y0] = -y0 # negative y because UnderlayCamera y is inverted + self._plane.geometry.positions.data[masks.y1] = -y1 - self.center_title() + self._plane.geometry.positions.update_full() - def center_title(self): - """Centers name of subplot.""" - if self._title_graphic is None: - raise AttributeError("No title graphic is set") + # note the negative y because UnderlayCamera y is inverted + self._resize_handler.geometry.positions.data[0] = [x1, -y1, 0] + self._resize_handler.geometry.positions.update_full() - self._title_graphic.world_object.position = (0, 0, 0) - self.docks["top"].center_graphic(self._title_graphic, zoom=1.5) - self._title_graphic.world_object.position_y = -3.5 + # set subplot title position + x = x0 + (w / 2) + y = y0 + (self.subplot_title.font_size / 2) + self.subplot_title.world_object.world.x = x + self.subplot_title.world_object.world.y = -y + + @property + def _fpl_plane(self) -> pygfx.Mesh: + """the plane mesh""" + return self._plane + + @property + def _fpl_resize_handler(self) -> pygfx.Points: + """resize handler point""" + return self._resize_handler + + def _canvas_resize_handler(self, *ev): + """triggered when canvas is resized""" + # render area, to account for any edge windows that might be present + # remember this frame also encapsulates the imgui toolbar which is + # part of the subplot so we do not subtract the toolbar height! + canvas_rect = self.figure.get_pygfx_render_area() + + self._rect._fpl_canvas_resized(canvas_rect) + self._reset_plane() + + @property + def subplot_title(self) -> TextGraphic: + return self._subplot_title + + def is_above(self, y0) -> bool: + # our bottom < other top + return self._rect.y1 < y0 + + def is_below(self, y1) -> bool: + # our top > other bottom + return self._rect.y0 > y1 + + def is_left_of(self, x0) -> bool: + # our right_edge < other left_edge + # self.x1 < other.x0 + return self._rect.x1 < x0 + + def is_right_of(self, x1) -> bool: + # self.x0 > other.x1 + return self._rect.x0 > x1 + + def overlaps(self, extent: np.ndarray) -> bool: + """returns whether this subplot overlaps with the given extent""" + x0, x1, y0, y1 = extent + return not any([self.is_above(y0), self.is_below(y1), self.is_left_of(x0), self.is_right_of(x1)]) class Dock(PlotArea): diff --git a/fastplotlib/layouts/_subplot_bbox.py b/fastplotlib/layouts/_subplot_bbox.py deleted file mode 100644 index e03c52b9a..000000000 --- a/fastplotlib/layouts/_subplot_bbox.py +++ /dev/null @@ -1,573 +0,0 @@ -from functools import partial - -import numpy as np -import pygfx - -from ..graphics import TextGraphic - - -class UnderlayCamera(pygfx.Camera): - """ - Same as pygfx.ScreenCoordsCamera but y-axis is inverted. - - So top right is (0, 0). This is easier to manage because we - often resize using the bottom right corner. - """ - - def _update_projection_matrix(self): - width, height = self._view_size - sx, sy, sz = 2 / width, 2 / height, 1 - dx, dy, dz = -1, 1, 0 # pygfx is -1, -1, 0 - m = sx, 0, 0, dx, 0, sy, 0, dy, 0, 0, sz, dz, 0, 0, 0, 1 - proj_matrix = np.array(m, dtype=float).reshape(4, 4) - proj_matrix.flags.writeable = False - return proj_matrix - - -""" -Each subplot is defined by a 2D plane mesh, a rectangle. -The rectangles are viewed using the UnderlayCamera where (0, 0) is the top left corner. -We can control the bbox of this rectangle by changing the x and y boundaries of the rectangle. - -Note how the y values of the plane mesh are negative, this is because of the UnderlayCamera. -We always just keep the positive y value, and make it negative only when setting the plane mesh. - -Illustration: - -(0, 0) --------------------------------------------------- ----------------------------------------------------------- ----------------------------------------------------------- ---------------(x0, -y0) --------------- (x1, -y0) -------- -------------------------|||||||||||||||------------------- -------------------------|||||||||||||||------------------- -------------------------|||||||||||||||------------------- -------------------------|||rectangle|||------------------- -------------------------|||||||||||||||------------------- -------------------------|||||||||||||||------------------- -------------------------|||||||||||||||------------------- ---------------(x0, -y1) --------------- (x1, -y1)--------- ----------------------------------------------------------- -------------------------------------------- (canvas_width, canvas_height) - -""" - - -class MeshMasks: - """Used set the x1, x1, y0, y1 positions of the mesh""" - x0 = np.array([ - [False, False, False], - [True, False, False], - [False, False, False], - [True, False, False], - ]) - - x1 = np.array([ - [True, False, False], - [False, False, False], - [True, False, False], - [False, False, False], - ]) - - y0 = np.array([ - [False, True, False], - [False, True, False], - [False, False, False], - [False, False, False], - ]) - - y1 = np.array([ - [False, False, False], - [False, False, False], - [False, True, False], - [False, True, False], - ]) - - -masks = MeshMasks - - -class RectManager: - def __init__(self, x: float, y: float, w: float, h: float, canvas_rect: tuple): - # initialize rect state arrays - # used to store internal state of the rect in both fractional screen space and absolute screen space - # the purpose of storing the fractional rect is that it remains constant when the canvas resizes - self._rect_frac = np.zeros(4, dtype=np.float64) - self._rect_screen_space = np.zeros(4, dtype=np.float64) - self._canvas_rect = np.asarray(canvas_rect) - - self._set((x, y, w, h)) - - def _set(self, rect): - """ - Using the passed rect which is either absolute screen space or fractional, - set the internal fractional and absolute screen space rects - """ - rect = np.asarray(rect) - for val, name in zip(rect, ["x-position", "y-position", "width", "height"]): - if val < 0: - raise ValueError(f"Invalid rect value < 0: {rect}") - - if (rect[2:] <= 1).all(): # fractional bbox - self._set_from_fract(rect) - - elif (rect[2:] > 1).all(): # bbox in already in screen coords coordinates - self._set_from_screen_space(rect) - - else: - raise ValueError(f"Invalid rect: {rect}") - - def _set_from_fract(self, rect): - """set rect from fractional representation""" - _, _, cw, ch = self._canvas_rect - mult = np.array([cw, ch, cw, ch]) - - # check that widths, heights are valid: - if rect[0] + rect[2] > 1: - raise ValueError("invalid fractional value: x + width > 1") - if rect[1] + rect[3] > 1: - raise ValueError("invalid fractional value: y + height > 1") - - # assign values, don't just change the reference - self._rect_frac[:] = rect - self._rect_screen_space[:] = self._rect_frac * mult - - def _set_from_screen_space(self, rect): - """set rect from screen space representation""" - _, _, cw, ch = self._canvas_rect - mult = np.array([cw, ch, cw, ch]) - # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 - # check that widths, heights are valid - if rect[0] + rect[2] > cw: - raise ValueError(f"invalid value: x + width > 1: {rect}") - if rect[1] + rect[3] > ch: - raise ValueError(f"invalid value: y + height > 1: {rect}") - - self._rect_frac[:] = rect / mult - self._rect_screen_space[:] = rect - - @property - def x(self) -> np.float64: - """x position""" - return self._rect_screen_space[0] - - @property - def y(self) -> np.float64: - """y position""" - return self._rect_screen_space[1] - - @property - def w(self) -> np.float64: - """width""" - return self._rect_screen_space[2] - - @property - def h(self) -> np.float64: - """height""" - return self._rect_screen_space[3] - - @property - def rect(self) -> np.ndarray: - """rect, (x, y, w, h)""" - return self._rect_screen_space - - @rect.setter - def rect(self, rect: np.ndarray | tuple): - self._set(rect) - - def _fpl_canvas_resized(self, canvas_rect: tuple): - # called by subplot when canvas is resized - self._canvas_rect[:] = canvas_rect - # set new rect using existing rect_frac since this remains constant regardless of resize - self._set(self._rect_frac) - - @property - def x0(self) -> np.float64: - """x0 position""" - return self.x - - @property - def x1(self) -> np.float64: - """x1 position""" - return self.x + self.w - - @property - def y0(self) -> np.float64: - """y0 position""" - return self.y - - @property - def y1(self) -> np.float64: - """y1 position""" - return self.y + self.h - - @classmethod - def from_extent(cls, extent, canvas_rect): - """create a RectManager from an extent""" - rect = cls.extent_to_rect(extent, canvas_rect) - return cls(*rect, canvas_rect) - - @property - def extent(self) -> np.ndarray: - """extent, (xmin, xmax, ymin, ymax)""" - # not actually stored, computed when needed - return np.asarray([self.x0, self.x1, self.y0, self.y1]) - - @extent.setter - def extent(self, extent): - """convert extent to rect""" - rect = RectManager.extent_to_rect(extent, canvas_rect=self._canvas_rect) - - self._set(rect) - - @staticmethod - def extent_to_rect(extent, canvas_rect): - RectManager.validate_extent(extent, canvas_rect) - x0, x1, y0, y1 = extent - - # width and height - w = x1 - x0 - h = y1 - y0 - - x, y, w, h = x0, y0, w, h - - return x, y, w, h - - @staticmethod - def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): - extent = np.asarray(extent) - cx0, cy0, cw, ch = canvas_rect - - # make sure extent is valid - if (extent < 0).any(): - raise ValueError(f"extent must be non-negative, you have passed: {extent}") - - if extent[1] < 1 or extent[3] < 1: # if x1 < 1, or y1 < 1 - # if fractional rect, convert to full - if not (extent < 1).all(): # if x1 and y1 < 1, then all vals must be < 1 - raise ValueError( - f"if passing a fractional extent, all values must be fractional, you have passed: {extent}") - extent *= np.asarray([cw, cw, ch, ch]) - - x0, x1, y0, y1 = extent - - # width and height - w = x1 - x0 - h = y1 - y0 - - # check if x1 - x0 <= 0 - if w <= 0: - raise ValueError(f"extent x-range must be non-negative: {extent}") - - # check if y1 - y0 <= 0 - if h <= 0: - raise ValueError(f"extent y-range must be non-negative: {extent}") - - # calc canvas extent - cx1 = cx0 + cw - cy1 = cy0 + ch - canvas_extent = np.asarray([cx0, cx1, cy0, cy1]) - - if x0 < cx0 or x1 < cx0 or x0 > cx1 or x1 > cx1: - raise ValueError(f"extent: {extent} x-range is beyond the bounds of the canvas: {canvas_extent}") - if y0 < cy0 or y1 < cy0 or y0 > cy1 or y1 > cy1: - raise ValueError(f"extent: {extent} y-range is beyond the bounds of the canvas: {canvas_extent}") - - def __repr__(self): - s = f"{self._rect_frac}\n{self.rect}" - - return s - - -class Frame: - def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None, subplot_title: str = None): - """ - - Parameters - ---------- - figure - rect: (x, y, w, h) - in absolute screen space or fractional screen space, example if the canvas w, h is (100, 200) - a fractional rect of (0.1, 0.1, 0.5, 0.5) is (10, 10, 50, 100) in absolute screen space - - extent: (xmin, xmax, ymin, ymax) - extent of the frame in absolute screen coordinates or fractional screen coordinates - """ - self.figure = figure - figure.canvas.add_event_handler(self._canvas_resize_handler, "resize") - - if rect is not None: - self._rect = RectManager(*rect, figure.get_pygfx_render_area()) - elif extent is not None: - self._rect = RectManager.from_extent(extent, figure.get_pygfx_render_area()) - else: - raise ValueError("Must provide `rect` or `extent`") - - if subplot_title is None: - subplot_title = "" - self._subplot_title = TextGraphic(subplot_title, face_color="black") - - # init mesh of size 1 to graphically represent rect - geometry = pygfx.plane_geometry(1, 1) - material = pygfx.MeshBasicMaterial(pick_write=True) - self._plane = pygfx.Mesh(geometry, material) - - # otherwise text isn't visible - self._plane.world.z = 0.5 - - # create resize handler at point (x1, y1) - x1, y1 = self.extent[[1, 3]] - self._resize_handler = pygfx.Points( - pygfx.Geometry(positions=[[x1, -y1, 0]]), # y is inverted in UnderlayCamera - pygfx.PointsMarkerMaterial(marker="square", size=12, size_space="screen", pick_write=True) - ) - - self._reset_plane() - - self._world_object = pygfx.Group() - self._world_object.add(self._plane, self._resize_handler, self._subplot_title.world_object) - - @property - def extent(self) -> np.ndarray: - """extent, (xmin, xmax, ymin, ymax)""" - # not actually stored, computed when needed - return self._rect.extent - - @extent.setter - def extent(self, extent): - self._rect.extent = extent - self._reset_plane() - - @property - def rect(self) -> np.ndarray[int]: - """rect in absolute screen space, (x, y, w, h)""" - return self._rect.rect - - @rect.setter - def rect(self, rect: np.ndarray): - self._rect.rect = rect - self._reset_plane() - - def _reset_plane(self): - """reset the plane mesh using the current rect state""" - - x0, x1, y0, y1 = self._rect.extent - w = self._rect.w - - self._plane.geometry.positions.data[masks.x0] = x0 - self._plane.geometry.positions.data[masks.x1] = x1 - self._plane.geometry.positions.data[masks.y0] = -y0 # negative y because UnderlayCamera y is inverted - self._plane.geometry.positions.data[masks.y1] = -y1 - - self._plane.geometry.positions.update_full() - - # note the negative y because UnderlayCamera y is inverted - self._resize_handler.geometry.positions.data[0] = [x1, -y1, 0] - self._resize_handler.geometry.positions.update_full() - - # set subplot title position - x = x0 + (w / 2) - y = y0 + (self.subplot_title.font_size / 2) - self.subplot_title.world_object.world.x = x - self.subplot_title.world_object.world.y = -y - - @property - def _fpl_plane(self) -> pygfx.Mesh: - """the plane mesh""" - return self._plane - - @property - def _fpl_resize_handler(self) -> pygfx.Points: - """resize handler point""" - return self._resize_handler - - def _canvas_resize_handler(self, *ev): - """triggered when canvas is resized""" - # render area, to account for any edge windows that might be present - # remember this frame also encapsulates the imgui toolbar which is - # part of the subplot so we do not subtract the toolbar height! - canvas_rect = self.figure.get_pygfx_render_area() - - self._rect._fpl_canvas_resized(canvas_rect) - self._reset_plane() - - @property - def subplot_title(self) -> TextGraphic: - return self._subplot_title - - def is_above(self, y0) -> bool: - # our bottom < other top - return self._rect.y1 < y0 - - def is_below(self, y1) -> bool: - # our top > other bottom - return self._rect.y0 > y1 - - def is_left_of(self, x0) -> bool: - # our right_edge < other left_edge - # self.x1 < other.x0 - return self._rect.x1 < x0 - - def is_right_of(self, x1) -> bool: - # self.x0 > other.x1 - return self._rect.x0 > x1 - - def overlaps(self, extent: np.ndarray) -> bool: - """returns whether this subplot overlaps with the given extent""" - x0, x1, y0, y1 = extent - return not any([self.is_above(y0), self.is_below(y1), self.is_left_of(x0), self.is_right_of(x1)]) - - -class BaseLayout: - def __init__(self, renderer: pygfx.WgpuRenderer, frames: tuple[Frame]): - self._renderer = renderer - self._frames = frames - - def set_rect(self, subplot, rect: np.ndarray | list | tuple): - raise NotImplementedError - - def set_extent(self, subplot, extent: np.ndarray | list | tuple): - raise NotImplementedError - - def _canvas_resize_handler(self, ev): - pass - - @property - def spacing(self) -> int: - pass - - -class GridLayout(BaseLayout): - def __init__(self, figure, frames: tuple[Frame]): - super().__init__(figure, frames) - - def set_rect(self, subplot, rect: np.ndarray | list | tuple): - raise NotImplementedError("set_rect() not implemented for GridLayout which is an auto layout manager") - - def set_extent(self, subplot, extent: np.ndarray | list | tuple): - raise NotImplementedError("set_extent() not implemented for GridLayout which is an auto layout manager") - - def _fpl_set_subplot_viewport_rect(self): - pass - - def _fpl_set_subplot_dock_viewport_rect(self): - pass - - -class FlexLayout(BaseLayout): - def __init__(self, renderer, get_canvas_rect: callable, frames: tuple[Frame]): - super().__init__(renderer, frames) - - self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array([np.nan, np.nan]) - - self._get_canvas_rect = get_canvas_rect - - self._active_action: str | None = None - self._active_frame: Frame | None = None - - for frame in self._frames: - frame._fpl_plane.add_event_handler(partial(self._action_start, frame, "move"), "pointer_down") - frame._fpl_resize_handler.add_event_handler( - partial(self._action_start, frame, "resize"), "pointer_down" - ) - frame._fpl_resize_handler.add_event_handler(self._highlight_resize_handler, "pointer_enter") - frame._fpl_resize_handler.add_event_handler(self._unhighlight_resize_handler, "pointer_leave") - - self._renderer.add_event_handler(self._action_iter, "pointer_move") - self._renderer.add_event_handler(self._action_end, "pointer_up") - - def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: - delta_x, delta_y = delta - if self._active_action == "resize": - # subtract only from x1, y1 - new_extent = self._active_frame.extent - np.asarray([0, delta_x, 0, delta_y]) - else: - # moving - new_extent = self._active_frame.extent - np.asarray([delta_x, delta_x, delta_y, delta_y]) - - x0, x1, y0, y1 = new_extent - w = x1 - x0 - h = y1 - y0 - - # make sure width and height are valid - # min width, height is 50px - if w <= 50: # width > 0 - new_extent[:2] = self._active_frame.extent[:2] - - if h <= 50: # height > 0 - new_extent[2:] = self._active_frame.extent[2:] - - # ignore movement if this would cause an overlap - for frame in self._frames: - if frame is self._active_frame: - continue - - if frame.overlaps(new_extent): - # we have an overlap, need to ignore one or more deltas - # ignore x - if not frame.is_left_of(x0) or not frame.is_right_of(x1): - new_extent[:2] = self._active_frame.extent[:2] - - # ignore y - if not frame.is_above(y0) or not frame.is_below(y1): - new_extent[2:] = self._active_frame.extent[2:] - - # make sure all vals are non-negative - if (new_extent[:2] < 0).any(): - # ignore delta_x - new_extent[:2] = self._active_frame.extent[:2] - - if (new_extent[2:] < 0).any(): - # ignore delta_y - new_extent[2:] = self._active_frame.extent[2:] - - # canvas extent - cx0, cy0, cw, ch = self._get_canvas_rect() - - # check if new x-range is beyond canvas x-max - if (new_extent[:2] > cx0 + cw).any(): - new_extent[:2] = self._active_frame.extent[:2] - - # check if new y-range is beyond canvas y-max - if (new_extent[2:] > cy0 + ch).any(): - new_extent[2:] = self._active_frame.extent[2:] - - return new_extent - - def _action_start(self, frame: Frame, action: str, ev): - if ev.button == 1: - self._active_action = action - if action == "resize": - frame._fpl_resize_handler.material.color = (1, 0, 0) - elif action == "move": - pass - else: - raise ValueError - - self._active_frame = frame - self._last_pointer_pos[:] = ev.x, ev.y - - def _action_iter(self, ev): - if self._active_action is None: - return - - delta_x, delta_y = self._last_pointer_pos - (ev.x, ev.y) - new_extent = self._new_extent_from_delta((delta_x, delta_y)) - self._active_frame.extent = new_extent - self._last_pointer_pos[:] = ev.x, ev.y - - def _action_end(self, ev): - self._active_action = None - self._active_frame._fpl_resize_handler.material.color = (1, 1, 1) - self._last_pointer_pos[:] = np.nan - - def _highlight_resize_handler(self, ev): - if self._active_action == "resize": - return - - ev.target.material.color = (1, 1, 0) - - def _unhighlight_resize_handler(self, ev): - if self._active_action == "resize": - return - - ev.target.material.color = (1, 1, 1) From 538edc28014e19fb876614af57bb5b317177abca Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Mar 2025 13:49:14 -0500 Subject: [PATCH 18/82] progress --- fastplotlib/layouts/_engine.py | 8 +- fastplotlib/layouts/_figure.py | 300 +++++++++++++-------------- fastplotlib/layouts/_imgui_figure.py | 4 + fastplotlib/layouts/_rect.py | 2 +- fastplotlib/layouts/_subplot.py | 17 +- 5 files changed, 168 insertions(+), 163 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index bee94ae00..22f5f69a5 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -166,9 +166,10 @@ def _update_projection_matrix(self): class BaseLayout: - def __init__(self, renderer: pygfx.WgpuRenderer, subplots: list[Subplot]): + def __init__(self, renderer: pygfx.WgpuRenderer, subplots: list[Subplot], canvas_rect: tuple): self._renderer = renderer self._subplots = subplots + self._canvas_rect = canvas_rect def set_rect(self, subplot, rect: np.ndarray | list | tuple): raise NotImplementedError @@ -176,8 +177,9 @@ def set_rect(self, subplot, rect: np.ndarray | list | tuple): def set_extent(self, subplot, extent: np.ndarray | list | tuple): raise NotImplementedError - def _canvas_resize_handler(self, ev): - pass + def _fpl_canvas_resized(self, canvas_rect: tuple): + for subplot in self._subplots: + subplot._fpl_canvas_resized(canvas_rect) @property def spacing(self) -> int: diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index a1632345d..3b9f1e23e 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -741,161 +741,157 @@ def export(self, uri: str | Path | bytes, **kwargs): def open_popup(self, *args, **kwargs): warn("popups only supported by ImguiFigure") - def _fpl_set_subplot_viewport_rect(self, subplot: Subplot): - """ - Sets the viewport rect for the given subplot - """ - - if self.mode == "grid": - # row, col position of this subplot within the grid - row_ix, col_ix = self._subplot_grid_positions[subplot] - - # number of rows, cols in the grid - nrows, ncols = self.shape - - # get starting positions and dimensions for the pygfx portion of the canvas - # anything outside the pygfx portion of the canvas is for imgui - x0_canvas, y0_canvas, width_canvas, height_canvas = ( - self.get_pygfx_render_area() - ) - - # width of an individual subplot - width_subplot = width_canvas / ncols - # height of an individual subplot - height_subplot = height_canvas / nrows - - # x position of this subplot - x_pos = ( - ((col_ix - 1) * width_subplot) - + width_subplot - + x0_canvas - + self.spacing - ) - # y position of this subplot - y_pos = ( - ((row_ix - 1) * height_subplot) - + height_subplot - + y0_canvas - + self.spacing - ) - - if self.__class__.__name__ == "ImguiFigure" and subplot.toolbar: - # leave space for imgui toolbar - height_subplot -= IMGUI_TOOLBAR_HEIGHT - - # clip so that min (w, h) is always 1, otherwise JupyterRenderCanvas causes issues because it - # initializes with a width, height of (0, 0) - rect = np.array( - [ - x_pos, - y_pos, - width_subplot - self.spacing, - height_subplot - self.spacing, - ] - ).clip(min=[0, 0, 1, 1]) - - # adjust if a subplot dock is present - adjust = np.array( - [ - # add left dock size to x_pos - subplot.docks["left"].size, - # add top dock size to y_pos - subplot.docks["top"].size, - # remove left and right dock sizes from width - -subplot.docks["right"].size - subplot.docks["left"].size, - # remove top and bottom dock sizes from height - -subplot.docks["top"].size - subplot.docks["bottom"].size, - ] - ) - - subplot.viewport.rect = rect + adjust - - def _fpl_set_subplot_dock_viewport_rect(self, subplot, position): - """ - Sets the viewport rect for the given subplot dock - """ - - dock = subplot.docks[position] - - if dock.size == 0: - dock.viewport.rect = None - return - - if self.mode == "grid": - # row, col position of this subplot within the grid - row_ix, col_ix = self._subplot_grid_positions[subplot] - - # number of rows, cols in the grid - nrows, ncols = self.shape - - x0_canvas, y0_canvas, width_canvas, height_canvas = ( - self.get_pygfx_render_area() - ) - - # width of an individual subplot - width_subplot = width_canvas / ncols - # height of an individual subplot - height_subplot = height_canvas / nrows - - # calculate the rect based on the dock position - match position: - case "right": - x_pos = ( - ((col_ix - 1) * width_subplot) + (width_subplot * 2) - dock.size - ) - y_pos = ( - ((row_ix - 1) * height_subplot) + height_subplot + self.spacing - ) - width_viewport = dock.size - height_viewport = height_subplot - self.spacing - - case "left": - x_pos = ((col_ix - 1) * width_subplot) + width_subplot - y_pos = ( - ((row_ix - 1) * height_subplot) + height_subplot + self.spacing - ) - width_viewport = dock.size - height_viewport = height_subplot - self.spacing - - case "top": - x_pos = ( - ((col_ix - 1) * width_subplot) + width_subplot + self.spacing - ) - y_pos = ( - ((row_ix - 1) * height_subplot) + height_subplot + self.spacing - ) - width_viewport = width_subplot - self.spacing - height_viewport = dock.size - - case "bottom": - x_pos = ( - ((col_ix - 1) * width_subplot) + width_subplot + self.spacing - ) - y_pos = ( - ((row_ix - 1) * height_subplot) - + (height_subplot * 2) - - dock.size - ) - width_viewport = width_subplot - self.spacing - height_viewport = dock.size - - case _: - raise ValueError("invalid position") - - dock.viewport.rect = [ - x_pos + x0_canvas, - y_pos + y0_canvas, - width_viewport, - height_viewport, - ] + # def _fpl_set_subplot_viewport_rect(self, subplot: Subplot): + # """ + # Sets the viewport rect for the given subplot + # """ + # + # if self.mode == "grid": + # # row, col position of this subplot within the grid + # row_ix, col_ix = self._subplot_grid_positions[subplot] + # + # # number of rows, cols in the grid + # nrows, ncols = self.shape + # + # # get starting positions and dimensions for the pygfx portion of the canvas + # # anything outside the pygfx portion of the canvas is for imgui + # x0_canvas, y0_canvas, width_canvas, height_canvas = ( + # self.get_pygfx_render_area() + # ) + # + # # width of an individual subplot + # width_subplot = width_canvas / ncols + # # height of an individual subplot + # height_subplot = height_canvas / nrows + # + # # x position of this subplot + # x_pos = ( + # ((col_ix - 1) * width_subplot) + # + width_subplot + # + x0_canvas + # + self.spacing + # ) + # # y position of this subplot + # y_pos = ( + # ((row_ix - 1) * height_subplot) + # + height_subplot + # + y0_canvas + # + self.spacing + # ) + # + # if self.__class__.__name__ == "ImguiFigure" and subplot.toolbar: + # # leave space for imgui toolbar + # height_subplot -= IMGUI_TOOLBAR_HEIGHT + # + # # clip so that min (w, h) is always 1, otherwise JupyterRenderCanvas causes issues because it + # # initializes with a width, height of (0, 0) + # rect = np.array( + # [ + # x_pos, + # y_pos, + # width_subplot - self.spacing, + # height_subplot - self.spacing, + # ] + # ).clip(min=[0, 0, 1, 1]) + # + # # adjust if a subplot dock is present + # adjust = np.array( + # [ + # # add left dock size to x_pos + # subplot.docks["left"].size, + # # add top dock size to y_pos + # subplot.docks["top"].size, + # # remove left and right dock sizes from width + # -subplot.docks["right"].size - subplot.docks["left"].size, + # # remove top and bottom dock sizes from height + # -subplot.docks["top"].size - subplot.docks["bottom"].size, + # ] + # ) + # + # subplot.viewport.rect = rect + adjust + + # def _fpl_set_subplot_dock_viewport_rect(self, subplot, position): + # """ + # Sets the viewport rect for the given subplot dock + # """ + # + # dock = subplot.docks[position] + # + # if dock.size == 0: + # dock.viewport.rect = None + # return + # + # if self.mode == "grid": + # # row, col position of this subplot within the grid + # row_ix, col_ix = self._subplot_grid_positions[subplot] + # + # # number of rows, cols in the grid + # nrows, ncols = self.shape + # + # x0_canvas, y0_canvas, width_canvas, height_canvas = ( + # self.get_pygfx_render_area() + # ) + # + # # width of an individual subplot + # width_subplot = width_canvas / ncols + # # height of an individual subplot + # height_subplot = height_canvas / nrows + # + # # calculate the rect based on the dock position + # match position: + # case "right": + # x_pos = ( + # ((col_ix - 1) * width_subplot) + (width_subplot * 2) - dock.size + # ) + # y_pos = ( + # ((row_ix - 1) * height_subplot) + height_subplot + self.spacing + # ) + # width_viewport = dock.size + # height_viewport = height_subplot - self.spacing + # + # case "left": + # x_pos = ((col_ix - 1) * width_subplot) + width_subplot + # y_pos = ( + # ((row_ix - 1) * height_subplot) + height_subplot + self.spacing + # ) + # width_viewport = dock.size + # height_viewport = height_subplot - self.spacing + # + # case "top": + # x_pos = ( + # ((col_ix - 1) * width_subplot) + width_subplot + self.spacing + # ) + # y_pos = ( + # ((row_ix - 1) * height_subplot) + height_subplot + self.spacing + # ) + # width_viewport = width_subplot - self.spacing + # height_viewport = dock.size + # + # case "bottom": + # x_pos = ( + # ((col_ix - 1) * width_subplot) + width_subplot + self.spacing + # ) + # y_pos = ( + # ((row_ix - 1) * height_subplot) + # + (height_subplot * 2) + # - dock.size + # ) + # width_viewport = width_subplot - self.spacing + # height_viewport = dock.size + # + # case _: + # raise ValueError("invalid position") + # + # dock.viewport.rect = [ + # x_pos + x0_canvas, + # y_pos + y0_canvas, + # width_viewport, + # height_viewport, + # ] def _set_viewport_rects(self, *ev): """set the viewport rects for all subplots, *ev argument is not used, exists because of renderer resize event""" - - for subplot in self: - self._fpl_set_subplot_viewport_rect(subplot) - for dock_pos in subplot.docks.keys(): - self._fpl_set_subplot_dock_viewport_rect(subplot, dock_pos) + self.layout._fpl_canvas_resized(self.get_pygfx_render_area()) def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: """ diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index 2e77f350d..14cf77456 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -21,6 +21,8 @@ class ImguiFigure(Figure): def __init__( self, shape: list[tuple[int, int, int, int]] | tuple[int, int] = (1, 1), + rects=None, + extents=None, cameras: ( Literal["2d", "3d"] | Iterable[Iterable[Literal["2d", "3d"]]] @@ -52,6 +54,8 @@ def __init__( super().__init__( shape=shape, + rects=rects, + extents=extents, cameras=cameras, controller_types=controller_types, controller_ids=controller_ids, diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index d8ea480bf..4fc7df11d 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -90,7 +90,7 @@ def rect(self, rect: np.ndarray | tuple): self._set(rect) def _fpl_canvas_resized(self, canvas_rect: tuple): - # called by subplot when canvas is resized + # called by layout when canvas is resized self._canvas_rect[:] = canvas_rect # set new rect using existing rect_frac since this remains constant regardless of resize self._set(self._rect_frac) diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 6bdccdb57..d1dee6ace 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -255,6 +255,7 @@ def extent(self) -> np.ndarray: def extent(self, extent): self._rect.extent = extent self._reset_plane() + self._reset_viewport_rect() @property def rect(self) -> np.ndarray[int]: @@ -265,6 +266,12 @@ def rect(self) -> np.ndarray[int]: def rect(self, rect: np.ndarray): self._rect.rect = rect self._reset_plane() + self._reset_viewport_rect() + + def _reset_viewport_rect(self): + rect = self.rect + viewport_rect = rect - np.array([1, self.title.font_size - 1, 1, self.title.font_size - 2]) + self.viewport.rect = viewport_rect def _reset_plane(self): """reset the plane mesh using the current rect state""" @@ -299,15 +306,11 @@ def _fpl_resize_handler(self) -> pygfx.Points: """resize handler point""" return self._resize_handler - def _canvas_resize_handler(self, *ev): - """triggered when canvas is resized""" - # render area, to account for any edge windows that might be present - # remember this frame also encapsulates the imgui toolbar which is - # part of the subplot so we do not subtract the toolbar height! - canvas_rect = self.figure.get_pygfx_render_area() - + def _fpl_canvas_resized(self, canvas_rect): + """called by layout is resized""" self._rect._fpl_canvas_resized(canvas_rect) self._reset_plane() + self._reset_viewport_rect() @property def subplot_title(self) -> TextGraphic: From 67636800433a3ec578fd2b02c85b2a4e1f766c82 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Mar 2025 16:27:44 -0500 Subject: [PATCH 19/82] progress --- fastplotlib/layouts/_engine.py | 22 +++++++-------- fastplotlib/layouts/_figure.py | 49 +++++++++++++++++---------------- fastplotlib/layouts/_subplot.py | 25 ++++++++--------- 3 files changed, 47 insertions(+), 49 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 22f5f69a5..2f56c4680 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -190,23 +190,21 @@ def __len__(self): class FlexLayout(BaseLayout): - def __init__(self, renderer, get_canvas_rect: callable, subplots: list[Subplot]): - super().__init__(renderer, subplots) + def __init__(self, renderer, subplots: list[Subplot], canvas_rect: tuple): + super().__init__(renderer, subplots, canvas_rect) self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array([np.nan, np.nan]) - self._get_canvas_rect = get_canvas_rect - self._active_action: str | None = None self._active_subplot: Subplot | None = None for frame in self._subplots: frame._fpl_plane.add_event_handler(partial(self._action_start, frame, "move"), "pointer_down") - frame._fpl_resize_handler.add_event_handler( + frame._fpl_resize_handle.add_event_handler( partial(self._action_start, frame, "resize"), "pointer_down" ) - frame._fpl_resize_handler.add_event_handler(self._highlight_resize_handler, "pointer_enter") - frame._fpl_resize_handler.add_event_handler(self._unhighlight_resize_handler, "pointer_leave") + frame._fpl_resize_handle.add_event_handler(self._highlight_resize_handler, "pointer_enter") + frame._fpl_resize_handle.add_event_handler(self._unhighlight_resize_handler, "pointer_leave") self._renderer.add_event_handler(self._action_iter, "pointer_move") self._renderer.add_event_handler(self._action_end, "pointer_up") @@ -257,7 +255,7 @@ def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: new_extent[2:] = self._active_subplot.extent[2:] # canvas extent - cx0, cy0, cw, ch = self._get_canvas_rect() + cx0, cy0, cw, ch = self._canvas_rect # check if new x-range is beyond canvas x-max if (new_extent[:2] > cx0 + cw).any(): @@ -273,7 +271,7 @@ def _action_start(self, subplot: Subplot, action: str, ev): if ev.button == 1: self._active_action = action if action == "resize": - subplot._fpl_resize_handler.material.color = (1, 0, 0) + subplot._fpl_resize_handle.material.color = (1, 0, 0) elif action == "move": pass else: @@ -293,7 +291,7 @@ def _action_iter(self, ev): def _action_end(self, ev): self._active_action = None - self._active_subplot._fpl_resize_handler.material.color = (1, 1, 1) + self._active_subplot._fpl_resize_handle.material.color = (1, 1, 1) self._last_pointer_pos[:] = np.nan def _highlight_resize_handler(self, ev): @@ -316,8 +314,8 @@ def remove_subplot(self): class GridLayout(FlexLayout): - def __init__(self, renderer, get_canvas_rect: callable, subplots: list[Subplot]): - super().__init__(renderer, subplots) + def __init__(self, renderer, subplots: list[Subplot], canvas_rect: tuple): + super().__init__(renderer, subplots, canvas_rect) # {Subplot: (row_ix, col_ix)}, dict mapping subplots to their row and col index in the grid layout self._subplot_grid_position: dict[Subplot, tuple[int, int]] diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 3b9f1e23e..31baf68c9 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -101,21 +101,23 @@ def __init__( """ if rects is not None: - if not all(isinstance(v, (np.ndarray, tuple, list)) for v in shape): + if not all(isinstance(v, (np.ndarray, tuple, list)) for v in rects): raise TypeError( f"rects must a list of arrays, tuples, or lists of rects (x, y, w, h), you have passed: {rects}" ) n_subplots = len(rects) layout_mode = "rect" + extents = [None] * n_subplots elif extents is not None: - if not all(isinstance(v, (np.ndarray, tuple, list)) for v in shape): + if not all(isinstance(v, (np.ndarray, tuple, list)) for v in extents): raise TypeError( f"extents must a list of arrays, tuples, or lists of extents (xmin, xmax, ymin, ymax), " f"you have passed: {extents}" ) n_subplots = len(extents) layout_mode = "extent" + rects = [None] * n_subplots else: if not all(isinstance(v, (int, np.integer)) for v in shape): @@ -144,7 +146,7 @@ def __init__( if names is not None: subplot_names = np.asarray(names).flatten() - if subplot_names.size != len(self): + if subplot_names.size != n_subplots: raise ValueError( "must provide same number of subplot `names` as specified by Figure `shape`" ) @@ -159,26 +161,26 @@ def __init__( if isinstance(cameras, str): # create the array representing the views for each subplot in the grid - cameras = np.array([cameras] * len(self)) + cameras = np.array([cameras] * n_subplots) # list/tuple -> array if necessary cameras = np.asarray(cameras).flatten() - if cameras.size != len(self): + if cameras.size != n_subplots: raise ValueError( - f"Number of cameras: {cameras.size} does not match the number of subplots: {len(self)}" + f"Number of cameras: {cameras.size} does not match the number of subplots: {n_subplots}" ) # create the cameras - subplot_cameras = np.empty(len(self), dtype=object) - for index in range(len(self)): + subplot_cameras = np.empty(n_subplots, dtype=object) + for index in range(n_subplots): subplot_cameras[index] = create_camera(camera_type=cameras[index]) # if controller instances have been specified for each subplot if controllers is not None: # one controller for all subplots if isinstance(controllers, pygfx.Controller): - controllers = [controllers] * len(self) + controllers = [controllers] * n_subplots # individual controller instance specified for each subplot else: @@ -198,25 +200,25 @@ def __init__( subplot_controllers: np.ndarray[pygfx.Controller] = np.asarray( controllers ).flatten() - if not subplot_controllers.size == len(self): + if not subplot_controllers.size == n_subplots: raise ValueError( f"number of controllers passed must be the same as the number of subplots specified " - f"by shape: {len(self)}. You have passed: {subplot_controllers.size} controllers" + f"by shape: {n_subplots}. You have passed: {subplot_controllers.size} controllers" ) from None - for index in range(len(self)): + for index in range(n_subplots): subplot_controllers[index].add_camera(subplot_cameras[index]) # parse controller_ids and controller_types to make desired controller for each subplot else: if controller_ids is None: # individual controller for each subplot - controller_ids = np.arange(len(self)) + controller_ids = np.arange(n_subplots) elif isinstance(controller_ids, str): if controller_ids == "sync": # this will end up creating one controller to control the camera of every subplot - controller_ids = np.zeros(len(self), dtype=int) + controller_ids = np.zeros(n_subplots, dtype=int) else: raise ValueError( f"`controller_ids` must be one of 'sync', an array/list of subplot names, or an array/list of " @@ -246,7 +248,7 @@ def __init__( ) # initialize controller_ids array - ids_init = np.arange(len(self)) + ids_init = np.arange(n_subplots) # set id based on subplot position for each synced sublist for row_ix, sublist in enumerate(controller_ids): @@ -271,18 +273,18 @@ def __init__( f"you have passed: {controller_ids}" ) - if controller_ids.size != len(self): + if controller_ids.size != n_subplots: 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(["default"] * len(self)) + controller_types = np.array(["default"] * n_subplots) # valid controller types if isinstance(controller_types, str): - controller_types = np.array([controller_types] * len(self)) + controller_types = np.array([controller_types] * n_subplots) controller_types: np.ndarray[pygfx.Controller] = np.asarray( controller_types @@ -302,7 +304,7 @@ def __init__( ) # make the real controllers for each subplot - subplot_controllers = np.empty(shape=len(self), dtype=object) + subplot_controllers = np.empty(shape=n_subplots, dtype=object) for cid in np.unique(controller_ids): cont_type = controller_types[controller_ids == cid] if np.unique(cont_type).size > 1: @@ -341,7 +343,7 @@ def __init__( ) else: - self._subplots = np.ndarray[Subplot] = np.empty(shape=n_subplots, dtype=object) + self._subplots: np.ndarray[Subplot] = np.empty(shape=n_subplots, dtype=object) for i in range(n_subplots): camera = subplot_cameras[i] @@ -371,10 +373,10 @@ def __init__( self._subplots[i] = subplot if layout_mode == "grid": - self._layout = GridLayout(self.renderer, self.get_pygfx_render_area, subplots=self._subplots) + self._layout = GridLayout(self.renderer, subplots=self._subplots, canvas_rect=self.get_pygfx_render_area()) elif layout_mode == "rect" or layout_mode == "extent": - self._layout = FlexLayout(self.renderer, self.get_pygfx_render_area, subplots=self._subplots) + self._layout = FlexLayout(self.renderer, subplots=self._subplots, canvas_rect=self.get_pygfx_render_area()) self._underlay_camera = UnderlayCamera() @@ -478,7 +480,7 @@ def __getitem__(self, index: str | int | tuple[int, int]) -> Subplot: def _render(self, draw=True): # draw the underlay planes - self.renderer.render(self._underlay_scene, self._underlay_camera) + self.renderer.render(self._underlay_scene, self._underlay_camera, flush=False) # call the animation functions before render self._call_animate_functions(self._animate_funcs_pre) @@ -494,6 +496,7 @@ def _start_render(self): """start render cycle""" self.canvas.request_draw(self._render) + def show( self, autoscale: bool = True, diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index d1dee6ace..2e00e3f47 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -174,15 +174,16 @@ def __init__( # create resize handler at point (x1, y1) x1, y1 = self.extent[[1, 3]] - self._resize_handler = pygfx.Points( + self._resize_handle = pygfx.Points( pygfx.Geometry(positions=[[x1, -y1, 0]]), # y is inverted in UnderlayCamera pygfx.PointsMarkerMaterial(marker="square", size=12, size_space="screen", pick_write=True) ) self._reset_plane() + self._reset_viewport_rect() self._world_object = pygfx.Group() - self._world_object.add(self._plane, self._resize_handler, self._title_graphic.world_object) + self._world_object.add(self._plane, self._resize_handle, self._title_graphic.world_object) @property def axes(self) -> Axes: @@ -270,7 +271,7 @@ def rect(self, rect: np.ndarray): def _reset_viewport_rect(self): rect = self.rect - viewport_rect = rect - np.array([1, self.title.font_size - 1, 1, self.title.font_size - 2]) + viewport_rect = rect - np.array([1, -self.title.font_size - 1, 1, -self.title.font_size - 2]) self.viewport.rect = viewport_rect def _reset_plane(self): @@ -287,14 +288,14 @@ def _reset_plane(self): self._plane.geometry.positions.update_full() # note the negative y because UnderlayCamera y is inverted - self._resize_handler.geometry.positions.data[0] = [x1, -y1, 0] - self._resize_handler.geometry.positions.update_full() + self._resize_handle.geometry.positions.data[0] = [x1, -y1, 0] + self._resize_handle.geometry.positions.update_full() # set subplot title position x = x0 + (w / 2) - y = y0 + (self.subplot_title.font_size / 2) - self.subplot_title.world_object.world.x = x - self.subplot_title.world_object.world.y = -y + y = y0 + (self.title.font_size / 2) + self.title.world_object.world.x = x + self.title.world_object.world.y = -y @property def _fpl_plane(self) -> pygfx.Mesh: @@ -302,9 +303,9 @@ def _fpl_plane(self) -> pygfx.Mesh: return self._plane @property - def _fpl_resize_handler(self) -> pygfx.Points: + def _fpl_resize_handle(self) -> pygfx.Points: """resize handler point""" - return self._resize_handler + return self._resize_handle def _fpl_canvas_resized(self, canvas_rect): """called by layout is resized""" @@ -312,10 +313,6 @@ def _fpl_canvas_resized(self, canvas_rect): self._reset_plane() self._reset_viewport_rect() - @property - def subplot_title(self) -> TextGraphic: - return self._subplot_title - def is_above(self, y0) -> bool: # our bottom < other top return self._rect.y1 < y0 From 194eef43eb1cee03d123b5de0dba4ac12e1fb65f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 6 Mar 2025 21:20:00 -0500 Subject: [PATCH 20/82] almost there --- fastplotlib/layouts/_engine.py | 24 +++++++++++++++++++++++- fastplotlib/layouts/_figure.py | 4 ---- fastplotlib/layouts/_subplot.py | 15 +++++++++------ fastplotlib/layouts/_utils.py | 4 ++++ fastplotlib/ui/_subplot_toolbar.py | 10 +++++----- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 2f56c4680..751667079 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -171,6 +171,21 @@ def __init__(self, renderer: pygfx.WgpuRenderer, subplots: list[Subplot], canvas self._subplots = subplots self._canvas_rect = canvas_rect + def _inside_render_rect(self, subplot: Subplot, pos: tuple[int, int]) -> bool: + """whether the pos is within the render area, used for filtering out pointer events""" + rect = subplot._fpl_get_render_rect() + + x0, y0 = rect[:2] + + x1 = x0 + rect[2] + y1 = y0 + rect[3] + + if (x0 < pos[0] < x1) and (y0 < pos[1] < y1): + return True + + return False + + def set_rect(self, subplot, rect: np.ndarray | list | tuple): raise NotImplementedError @@ -178,6 +193,7 @@ def set_extent(self, subplot, extent: np.ndarray | list | tuple): raise NotImplementedError def _fpl_canvas_resized(self, canvas_rect: tuple): + self._canvas_rect = canvas_rect for subplot in self._subplots: subplot._fpl_canvas_resized(canvas_rect) @@ -268,6 +284,9 @@ def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: return new_extent def _action_start(self, subplot: Subplot, action: str, ev): + if self._inside_render_rect(subplot, pos=(ev.x, ev.y)): + return + if ev.button == 1: self._active_action = action if action == "resize": @@ -291,7 +310,10 @@ def _action_iter(self, ev): def _action_end(self, ev): self._active_action = None - self._active_subplot._fpl_resize_handle.material.color = (1, 1, 1) + if self._active_subplot is not None: + self._active_subplot._fpl_resize_handle.material.color = (1, 1, 1) + self._active_subplot = None + self._last_pointer_pos[:] = np.nan def _highlight_resize_handler(self, ev): diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 31baf68c9..02c63f6aa 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -18,10 +18,6 @@ from .. import ImageGraphic -# number of pixels taken by the imgui toolbar when present -IMGUI_TOOLBAR_HEIGHT = 39 - - class Figure: def __init__( self, diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 2e00e3f47..ef38c5f61 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -7,7 +7,7 @@ from ._rect import RectManager from ..graphics import TextGraphic -from ._utils import create_camera, create_controller +from ._utils import create_camera, create_controller, IMGUI_TOOLBAR_HEIGHT from ._plot_area import PlotArea from ._graphic_methods_mixin import GraphicMethodsMixin from ..graphics._axes import Axes @@ -162,11 +162,12 @@ def __init__( title_text = "" else: title_text = name - self._title_graphic = TextGraphic(title_text, face_color="black") + self._title_graphic = TextGraphic(title_text, font_size=16, face_color="white") + # self._title_graphic.world_object.material.weight_offset = 50 # init mesh of size 1 to graphically represent rect geometry = pygfx.plane_geometry(1, 1) - material = pygfx.MeshBasicMaterial(pick_write=True) + material = pygfx.MeshBasicMaterial(color=(0.1, 0.1, 0.1), pick_write=True) self._plane = pygfx.Mesh(geometry, material) # otherwise text isn't visible @@ -270,9 +271,11 @@ def rect(self, rect: np.ndarray): self._reset_viewport_rect() def _reset_viewport_rect(self): + self.viewport.rect = self._fpl_get_render_rect() + + def _fpl_get_render_rect(self): rect = self.rect - viewport_rect = rect - np.array([1, -self.title.font_size - 1, 1, -self.title.font_size - 2]) - self.viewport.rect = viewport_rect + return rect + np.array([1, self.title.font_size + 2 + 4, -2, -self.title.font_size - 2 - 4 - IMGUI_TOOLBAR_HEIGHT - 1]) def _reset_plane(self): """reset the plane mesh using the current rect state""" @@ -295,7 +298,7 @@ def _reset_plane(self): x = x0 + (w / 2) y = y0 + (self.title.font_size / 2) self.title.world_object.world.x = x - self.title.world_object.world.y = -y + self.title.world_object.world.y = -y - 2 @property def _fpl_plane(self) -> pygfx.Mesh: diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index b42971570..a3745c03d 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -6,6 +6,10 @@ from ..utils.gui import BaseRenderCanvas, RenderCanvas +# number of pixels taken by the imgui toolbar when present +IMGUI_TOOLBAR_HEIGHT = 39 + + def make_canvas_and_renderer( canvas: str | BaseRenderCanvas | Texture | None, renderer: Renderer | None, diff --git a/fastplotlib/ui/_subplot_toolbar.py b/fastplotlib/ui/_subplot_toolbar.py index 7d183bf6d..b65b398ef 100644 --- a/fastplotlib/ui/_subplot_toolbar.py +++ b/fastplotlib/ui/_subplot_toolbar.py @@ -2,6 +2,7 @@ from ..layouts._subplot import Subplot from ._base import Window +from ..layouts._utils import IMGUI_TOOLBAR_HEIGHT class SubplotToolbar(Window): @@ -16,15 +17,14 @@ def __init__(self, subplot: Subplot, fa_icons: imgui.ImFont): def update(self): # get subplot rect - x, y, width, height = self._subplot.viewport.rect - y += self._subplot.docks["bottom"].size + x, y, width, height = self._subplot.rect # place the toolbar window below the subplot - pos = (x, y + height) + pos = (x + 1, y + height - IMGUI_TOOLBAR_HEIGHT) - imgui.set_next_window_size((width, 0)) + imgui.set_next_window_size((width - 16, 0)) imgui.set_next_window_pos(pos) - flags = imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_title_bar + flags = imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_title_bar | imgui.WindowFlags_.no_background imgui.begin(f"Toolbar-{hex(id(self._subplot))}", p_open=None, flags=flags) From 8990757786198c1e4339965ec0a4230bf9ca5077 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Mar 2025 02:07:31 -0500 Subject: [PATCH 21/82] formatting, subplot toolbar tweaks --- fastplotlib/layouts/_engine.py | 43 ++++++++---- fastplotlib/layouts/_figure.py | 17 +++-- fastplotlib/layouts/_rect.py | 11 +++- fastplotlib/layouts/_subplot.py | 102 +++++++++++++++++++---------- fastplotlib/ui/_subplot_toolbar.py | 6 +- 5 files changed, 123 insertions(+), 56 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 751667079..45e7883e2 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -166,7 +166,9 @@ def _update_projection_matrix(self): class BaseLayout: - def __init__(self, renderer: pygfx.WgpuRenderer, subplots: list[Subplot], canvas_rect: tuple): + def __init__( + self, renderer: pygfx.WgpuRenderer, subplots: list[Subplot], canvas_rect: tuple + ): self._renderer = renderer self._subplots = subplots self._canvas_rect = canvas_rect @@ -185,7 +187,6 @@ def _inside_render_rect(self, subplot: Subplot, pos: tuple[int, int]) -> bool: return False - def set_rect(self, subplot, rect: np.ndarray | list | tuple): raise NotImplementedError @@ -209,18 +210,26 @@ class FlexLayout(BaseLayout): def __init__(self, renderer, subplots: list[Subplot], canvas_rect: tuple): super().__init__(renderer, subplots, canvas_rect) - self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array([np.nan, np.nan]) + self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( + [np.nan, np.nan] + ) self._active_action: str | None = None self._active_subplot: Subplot | None = None for frame in self._subplots: - frame._fpl_plane.add_event_handler(partial(self._action_start, frame, "move"), "pointer_down") + frame._fpl_plane.add_event_handler( + partial(self._action_start, frame, "move"), "pointer_down" + ) frame._fpl_resize_handle.add_event_handler( partial(self._action_start, frame, "resize"), "pointer_down" ) - frame._fpl_resize_handle.add_event_handler(self._highlight_resize_handler, "pointer_enter") - frame._fpl_resize_handle.add_event_handler(self._unhighlight_resize_handler, "pointer_leave") + frame._fpl_resize_handle.add_event_handler( + self._highlight_resize_handler, "pointer_enter" + ) + frame._fpl_resize_handle.add_event_handler( + self._unhighlight_resize_handler, "pointer_leave" + ) self._renderer.add_event_handler(self._action_iter, "pointer_move") self._renderer.add_event_handler(self._action_end, "pointer_up") @@ -229,10 +238,14 @@ def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: delta_x, delta_y = delta if self._active_action == "resize": # subtract only from x1, y1 - new_extent = self._active_subplot.extent - np.asarray([0, delta_x, 0, delta_y]) + new_extent = self._active_subplot.extent - np.asarray( + [0, delta_x, 0, delta_y] + ) else: # moving - new_extent = self._active_subplot.extent - np.asarray([delta_x, delta_x, delta_y, delta_y]) + new_extent = self._active_subplot.extent - np.asarray( + [delta_x, delta_x, delta_y, delta_y] + ) x0, x1, y0, y1 = new_extent w = x1 - x0 @@ -311,7 +324,7 @@ def _action_iter(self, ev): def _action_end(self, ev): self._active_action = None if self._active_subplot is not None: - self._active_subplot._fpl_resize_handle.material.color = (1, 1, 1) + self._active_subplot._fpl_resize_handle.material.color = (0.5, 0.5, 0.5) self._active_subplot = None self._last_pointer_pos[:] = np.nan @@ -320,13 +333,13 @@ def _highlight_resize_handler(self, ev): if self._active_action == "resize": return - ev.target.material.color = (1, 1, 0) + ev.target.material.color = (1, 1, 1) def _unhighlight_resize_handler(self, ev): if self._active_action == "resize": return - ev.target.material.color = (1, 1, 1) + ev.target.material.color = (0.5, 0.5, 0.5) def add_subplot(self): pass @@ -347,10 +360,14 @@ def shape(self) -> tuple[int, int]: pass def set_rect(self, subplot, rect: np.ndarray | list | tuple): - raise NotImplementedError("set_rect() not implemented for GridLayout which is an auto layout manager") + raise NotImplementedError( + "set_rect() not implemented for GridLayout which is an auto layout manager" + ) def set_extent(self, subplot, extent: np.ndarray | list | tuple): - raise NotImplementedError("set_extent() not implemented for GridLayout which is an auto layout manager") + raise NotImplementedError( + "set_extent() not implemented for GridLayout which is an auto layout manager" + ) def _fpl_set_subplot_viewport_rect(self): pass diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 02c63f6aa..6c790886e 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -339,7 +339,9 @@ def __init__( ) else: - self._subplots: np.ndarray[Subplot] = np.empty(shape=n_subplots, dtype=object) + self._subplots: np.ndarray[Subplot] = np.empty( + shape=n_subplots, dtype=object + ) for i in range(n_subplots): camera = subplot_cameras[i] @@ -369,10 +371,18 @@ def __init__( self._subplots[i] = subplot if layout_mode == "grid": - self._layout = GridLayout(self.renderer, subplots=self._subplots, canvas_rect=self.get_pygfx_render_area()) + self._layout = GridLayout( + self.renderer, + subplots=self._subplots, + canvas_rect=self.get_pygfx_render_area(), + ) elif layout_mode == "rect" or layout_mode == "extent": - self._layout = FlexLayout(self.renderer, subplots=self._subplots, canvas_rect=self.get_pygfx_render_area()) + self._layout = FlexLayout( + self.renderer, + subplots=self._subplots, + canvas_rect=self.get_pygfx_render_area(), + ) self._underlay_camera = UnderlayCamera() @@ -492,7 +502,6 @@ def _start_render(self): """start render cycle""" self.canvas.request_draw(self._render) - def show( self, autoscale: bool = True, diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index 4fc7df11d..a2073e35e 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -160,7 +160,8 @@ def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): # if fractional rect, convert to full if not (extent < 1).all(): # if x1 and y1 < 1, then all vals must be < 1 raise ValueError( - f"if passing a fractional extent, all values must be fractional, you have passed: {extent}") + f"if passing a fractional extent, all values must be fractional, you have passed: {extent}" + ) extent *= np.asarray([cw, cw, ch, ch]) x0, x1, y0, y1 = extent @@ -183,9 +184,13 @@ def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): canvas_extent = np.asarray([cx0, cx1, cy0, cy1]) if x0 < cx0 or x1 < cx0 or x0 > cx1 or x1 > cx1: - raise ValueError(f"extent: {extent} x-range is beyond the bounds of the canvas: {canvas_extent}") + raise ValueError( + f"extent: {extent} x-range is beyond the bounds of the canvas: {canvas_extent}" + ) if y0 < cy0 or y1 < cy0 or y0 > cy1 or y1 > cy1: - raise ValueError(f"extent: {extent} y-range is beyond the bounds of the canvas: {canvas_extent}") + raise ValueError( + f"extent: {extent} y-range is beyond the bounds of the canvas: {canvas_extent}" + ) def __repr__(self): s = f"{self._rect_frac}\n{self.rect}" diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index ef38c5f61..960e39874 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -43,33 +43,42 @@ class MeshMasks: """Used set the x1, x1, y0, y1 positions of the mesh""" - x0 = np.array([ - [False, False, False], - [True, False, False], - [False, False, False], - [True, False, False], - ]) - - x1 = np.array([ - [True, False, False], - [False, False, False], - [True, False, False], - [False, False, False], - ]) - - y0 = np.array([ - [False, True, False], - [False, True, False], - [False, False, False], - [False, False, False], - ]) - - y1 = np.array([ - [False, False, False], - [False, False, False], - [False, True, False], - [False, True, False], - ]) + + x0 = np.array( + [ + [False, False, False], + [True, False, False], + [False, False, False], + [True, False, False], + ] + ) + + x1 = np.array( + [ + [True, False, False], + [False, False, False], + [True, False, False], + [False, False, False], + ] + ) + + y0 = np.array( + [ + [False, True, False], + [False, True, False], + [False, False, False], + [False, False, False], + ] + ) + + y1 = np.array( + [ + [False, False, False], + [False, False, False], + [False, True, False], + [False, True, False], + ] + ) masks = MeshMasks @@ -154,7 +163,9 @@ def __init__( if rect is not None: self._rect = RectManager(*rect, self.get_figure().get_pygfx_render_area()) elif extent is not None: - self._rect = RectManager.from_extent(extent, self.get_figure().get_pygfx_render_area()) + self._rect = RectManager.from_extent( + extent, self.get_figure().get_pygfx_render_area() + ) else: raise ValueError("Must provide `rect` or `extent`") @@ -177,14 +188,18 @@ def __init__( x1, y1 = self.extent[[1, 3]] self._resize_handle = pygfx.Points( pygfx.Geometry(positions=[[x1, -y1, 0]]), # y is inverted in UnderlayCamera - pygfx.PointsMarkerMaterial(marker="square", size=12, size_space="screen", pick_write=True) + pygfx.PointsMarkerMaterial( + color=(0.5, 0.5, 0.5), marker="square", size=8, size_space="screen", pick_write=True + ), ) self._reset_plane() self._reset_viewport_rect() self._world_object = pygfx.Group() - self._world_object.add(self._plane, self._resize_handle, self._title_graphic.world_object) + self._world_object.add( + self._plane, self._resize_handle, self._title_graphic.world_object + ) @property def axes(self) -> Axes: @@ -274,8 +289,16 @@ def _reset_viewport_rect(self): self.viewport.rect = self._fpl_get_render_rect() def _fpl_get_render_rect(self): - rect = self.rect - return rect + np.array([1, self.title.font_size + 2 + 4, -2, -self.title.font_size - 2 - 4 - IMGUI_TOOLBAR_HEIGHT - 1]) + x, y, w, h = self.rect + + x += 1 # add 1 so a 1 pixel edge is visible + w -= 2 # subtract 2, so we get a 1 pixel edge on both sides + + y = y + 4 + self.title.font_size + 4 # add 4 pixels above and below title for better spacing + # adjust for spacing and 3 pixels for toolbar spacing + h = h - 4 - self.title.font_size - IMGUI_TOOLBAR_HEIGHT - 4 - 3 + + return x, y, w, h def _reset_plane(self): """reset the plane mesh using the current rect state""" @@ -285,7 +308,9 @@ def _reset_plane(self): self._plane.geometry.positions.data[masks.x0] = x0 self._plane.geometry.positions.data[masks.x1] = x1 - self._plane.geometry.positions.data[masks.y0] = -y0 # negative y because UnderlayCamera y is inverted + self._plane.geometry.positions.data[masks.y0] = ( + -y0 + ) # negative y because UnderlayCamera y is inverted self._plane.geometry.positions.data[masks.y1] = -y1 self._plane.geometry.positions.update_full() @@ -298,7 +323,7 @@ def _reset_plane(self): x = x0 + (w / 2) y = y0 + (self.title.font_size / 2) self.title.world_object.world.x = x - self.title.world_object.world.y = -y - 2 + self.title.world_object.world.y = -y - 4 # add 4 pixels for spacing @property def _fpl_plane(self) -> pygfx.Mesh: @@ -336,7 +361,14 @@ def is_right_of(self, x1) -> bool: def overlaps(self, extent: np.ndarray) -> bool: """returns whether this subplot overlaps with the given extent""" x0, x1, y0, y1 = extent - return not any([self.is_above(y0), self.is_below(y1), self.is_left_of(x0), self.is_right_of(x1)]) + return not any( + [ + self.is_above(y0), + self.is_below(y1), + self.is_left_of(x0), + self.is_right_of(x1), + ] + ) class Dock(PlotArea): diff --git a/fastplotlib/ui/_subplot_toolbar.py b/fastplotlib/ui/_subplot_toolbar.py index b65b398ef..cebd916e8 100644 --- a/fastplotlib/ui/_subplot_toolbar.py +++ b/fastplotlib/ui/_subplot_toolbar.py @@ -24,7 +24,11 @@ def update(self): imgui.set_next_window_size((width - 16, 0)) imgui.set_next_window_pos(pos) - flags = imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_title_bar | imgui.WindowFlags_.no_background + flags = ( + imgui.WindowFlags_.no_collapse + | imgui.WindowFlags_.no_title_bar + | imgui.WindowFlags_.no_background + ) imgui.begin(f"Toolbar-{hex(id(self._subplot))}", p_open=None, flags=flags) From 6e30b8b8fa3a2cce4cd75cf8688a4ce19f92501c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Mar 2025 18:09:59 -0500 Subject: [PATCH 22/82] cleanup --- fastplotlib/layouts/_engine.py | 139 --------------------------------- 1 file changed, 139 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 45e7883e2..29027dafa 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -26,145 +26,6 @@ def _update_projection_matrix(self): return proj_matrix -# class Frame: -# def __init__(self, figure, rect: np.ndarray = None, extent: np.ndarray = None, subplot_title: str = None): -# """ -# -# Parameters -# ---------- -# figure -# rect: (x, y, w, h) -# in absolute screen space or fractional screen space, example if the canvas w, h is (100, 200) -# a fractional rect of (0.1, 0.1, 0.5, 0.5) is (10, 10, 50, 100) in absolute screen space -# -# extent: (xmin, xmax, ymin, ymax) -# extent of the frame in absolute screen coordinates or fractional screen coordinates -# """ -# self.figure = figure -# figure.canvas.add_event_handler(self._canvas_resize_handler, "resize") -# -# if rect is not None: -# self._rect = RectManager(*rect, figure.get_pygfx_render_area()) -# elif extent is not None: -# self._rect = RectManager.from_extent(extent, figure.get_pygfx_render_area()) -# else: -# raise ValueError("Must provide `rect` or `extent`") -# -# if subplot_title is None: -# subplot_title = "" -# self._subplot_title = TextGraphic(subplot_title, face_color="black") -# -# # init mesh of size 1 to graphically represent rect -# geometry = pygfx.plane_geometry(1, 1) -# material = pygfx.MeshBasicMaterial(pick_write=True) -# self._plane = pygfx.Mesh(geometry, material) -# -# # otherwise text isn't visible -# self._plane.world.z = 0.5 -# -# # create resize handler at point (x1, y1) -# x1, y1 = self.extent[[1, 3]] -# self._resize_handler = pygfx.Points( -# pygfx.Geometry(positions=[[x1, -y1, 0]]), # y is inverted in UnderlayCamera -# pygfx.PointsMarkerMaterial(marker="square", size=12, size_space="screen", pick_write=True) -# ) -# -# self._reset_plane() -# -# self._world_object = pygfx.Group() -# self._world_object.add(self._plane, self._resize_handler, self._subplot_title.world_object) -# -# @property -# def extent(self) -> np.ndarray: -# """extent, (xmin, xmax, ymin, ymax)""" -# # not actually stored, computed when needed -# return self._rect.extent -# -# @extent.setter -# def extent(self, extent): -# self._rect.extent = extent -# self._reset_plane() -# -# @property -# def rect(self) -> np.ndarray[int]: -# """rect in absolute screen space, (x, y, w, h)""" -# return self._rect.rect -# -# @rect.setter -# def rect(self, rect: np.ndarray): -# self._rect.rect = rect -# self._reset_plane() -# -# def _reset_plane(self): -# """reset the plane mesh using the current rect state""" -# -# x0, x1, y0, y1 = self._rect.extent -# w = self._rect.w -# -# self._plane.geometry.positions.data[masks.x0] = x0 -# self._plane.geometry.positions.data[masks.x1] = x1 -# self._plane.geometry.positions.data[masks.y0] = -y0 # negative y because UnderlayCamera y is inverted -# self._plane.geometry.positions.data[masks.y1] = -y1 -# -# self._plane.geometry.positions.update_full() -# -# # note the negative y because UnderlayCamera y is inverted -# self._resize_handler.geometry.positions.data[0] = [x1, -y1, 0] -# self._resize_handler.geometry.positions.update_full() -# -# # set subplot title position -# x = x0 + (w / 2) -# y = y0 + (self.subplot_title.font_size / 2) -# self.subplot_title.world_object.world.x = x -# self.subplot_title.world_object.world.y = -y -# -# @property -# def _fpl_plane(self) -> pygfx.Mesh: -# """the plane mesh""" -# return self._plane -# -# @property -# def _fpl_resize_handler(self) -> pygfx.Points: -# """resize handler point""" -# return self._resize_handler -# -# def _canvas_resize_handler(self, *ev): -# """triggered when canvas is resized""" -# # render area, to account for any edge windows that might be present -# # remember this frame also encapsulates the imgui toolbar which is -# # part of the subplot so we do not subtract the toolbar height! -# canvas_rect = self.figure.get_pygfx_render_area() -# -# self._rect._fpl_canvas_resized(canvas_rect) -# self._reset_plane() -# -# @property -# def subplot_title(self) -> TextGraphic: -# return self._subplot_title -# -# def is_above(self, y0) -> bool: -# # our bottom < other top -# return self._rect.y1 < y0 -# -# def is_below(self, y1) -> bool: -# # our top > other bottom -# return self._rect.y0 > y1 -# -# def is_left_of(self, x0) -> bool: -# # our right_edge < other left_edge -# # self.x1 < other.x0 -# return self._rect.x1 < x0 -# -# def is_right_of(self, x1) -> bool: -# # self.x0 > other.x1 -# return self._rect.x0 > x1 -# -# def overlaps(self, extent: np.ndarray) -> bool: -# """returns whether this subplot overlaps with the given extent""" -# x0, x1, y0, y1 = extent -# return not any([self.is_above(y0), self.is_below(y1), self.is_left_of(x0), self.is_right_of(x1)]) - - class BaseLayout: def __init__( self, renderer: pygfx.WgpuRenderer, subplots: list[Subplot], canvas_rect: tuple From ad5b903f99f16ce263e6b8d859badab4a7f6fec2 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Mar 2025 21:49:22 -0500 Subject: [PATCH 23/82] docks --- fastplotlib/layouts/_subplot.py | 64 +++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 960e39874..ae0cbaa2e 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -7,7 +7,7 @@ from ._rect import RectManager from ..graphics import TextGraphic -from ._utils import create_camera, create_controller, IMGUI_TOOLBAR_HEIGHT +from ._utils import create_camera, create_controller, IMGUI, IMGUI_TOOLBAR_HEIGHT from ._plot_area import PlotArea from ._graphic_methods_mixin import GraphicMethodsMixin from ..graphics._axes import Axes @@ -139,7 +139,10 @@ def __init__( self._docks = dict() - self._toolbar = True + if IMGUI: + self._toolbar = True + else: + self._toolbar = False super(Subplot, self).__init__( parent=parent, @@ -246,7 +249,7 @@ def toolbar(self) -> bool: @toolbar.setter def toolbar(self, visible: bool): self._toolbar = bool(visible) - self.get_figure()._fpl_set_subplot_viewport_rect(self) + self.get_figure()._set_viewport_rects(self) def _render(self): self.axes.update_using_camera() @@ -286,17 +289,55 @@ def rect(self, rect: np.ndarray): self._reset_viewport_rect() def _reset_viewport_rect(self): - self.viewport.rect = self._fpl_get_render_rect() + # get rect of the render area + x, y, w, h = self._fpl_get_render_rect() + + s_left = self.docks["left"].size + s_top = self.docks["top"].size + s_right = self.docks["right"].size + s_bottom = self.docks["bottom"].size + + # top and bottom have same width + # subtract left and right dock sizes + w_top_bottom = w - s_left - s_right + # top and bottom have same x pos + x_top_bottom = x + s_left + + # set dock rects + self.docks["left"].viewport.rect = x, y, s_left, h + self.docks["top"].viewport.rect = x_top_bottom, y, w_top_bottom, s_top + self.docks["bottom"].viewport.rect = x_top_bottom, y + h - s_bottom, w_top_bottom, s_bottom + self.docks["right"].viewport.rect = x + w - s_right, y, s_right, h + + # calc subplot rect by adjusting for dock sizes + x += s_left + y += s_top + w -= s_left + s_right + h -= s_top + s_bottom + + # set subplot rect + self.viewport.rect = x, y, w, h + + def _fpl_get_render_rect(self) -> tuple[float, float, float, float]: + """ + Get the actual render area of the subplot, including the docks. - def _fpl_get_render_rect(self): + Excludes area taken by the subplot title and toolbar. Also adds a small amount of spacing around the subplot. + """ x, y, w, h = self.rect x += 1 # add 1 so a 1 pixel edge is visible w -= 2 # subtract 2, so we get a 1 pixel edge on both sides y = y + 4 + self.title.font_size + 4 # add 4 pixels above and below title for better spacing - # adjust for spacing and 3 pixels for toolbar spacing - h = h - 4 - self.title.font_size - IMGUI_TOOLBAR_HEIGHT - 4 - 3 + + if self.toolbar: + toolbar_space = IMGUI_TOOLBAR_HEIGHT + else: + toolbar_space = 0 + + # adjust for spacing and 4 pixels for more spacing + h = h - 4 - self.title.font_size - toolbar_space - 4 - 4 return x, y, w, h @@ -409,14 +450,7 @@ def size(self) -> int: @size.setter def size(self, s: int): self._size = s - if self.position == "top": - # TODO: treat title dock separately, do not allow user to change viewport stuff - return - - self.get_figure(self.parent)._fpl_set_subplot_viewport_rect(self.parent) - self.get_figure(self.parent)._fpl_set_subplot_dock_viewport_rect( - self.parent, self._position - ) + self.parent._reset_viewport_rect() def _render(self): if self.size == 0: From 500c05cf4495b513ee414e03fe79af753963aa0a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 7 Mar 2025 21:49:59 -0500 Subject: [PATCH 24/82] more stuff --- fastplotlib/layouts/__init__.py | 8 +------- fastplotlib/layouts/_engine.py | 4 +--- fastplotlib/layouts/_utils.py | 7 +++++++ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/fastplotlib/layouts/__init__.py b/fastplotlib/layouts/__init__.py index 4a4f45174..8fb1d54d8 100644 --- a/fastplotlib/layouts/__init__.py +++ b/fastplotlib/layouts/__init__.py @@ -1,11 +1,5 @@ from ._figure import Figure - -try: - import imgui_bundle -except ImportError: - IMGUI = False -else: - IMGUI = True +from ._utils import IMGUI if IMGUI: from ._imgui_figure import ImguiFigure diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 29027dafa..d485226f1 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -5,8 +5,6 @@ from ._subplot import Subplot -# from ..graphics import TextGraphic - class UnderlayCamera(pygfx.Camera): """ @@ -164,7 +162,7 @@ def _action_start(self, subplot: Subplot, action: str, ev): if ev.button == 1: self._active_action = action if action == "resize": - subplot._fpl_resize_handle.material.color = (1, 0, 0) + subplot._fpl_resize_handle.material.color = (1, 0, 1) elif action == "move": pass else: diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index a3745c03d..3f8d43f6a 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -5,6 +5,13 @@ from ..utils.gui import BaseRenderCanvas, RenderCanvas +try: + import imgui_bundle +except ImportError: + IMGUI = False +else: + IMGUI = True + # number of pixels taken by the imgui toolbar when present IMGUI_TOOLBAR_HEIGHT = 39 From 3d16ce137c6eb5a388fcc095f0bc19f1016ee62c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 00:04:36 -0500 Subject: [PATCH 25/82] grid works --- fastplotlib/layouts/_engine.py | 120 ++++++++++++------ fastplotlib/layouts/_figure.py | 49 ++----- fastplotlib/layouts/_plot_area.py | 2 +- fastplotlib/layouts/_rect.py | 4 +- fastplotlib/layouts/_subplot.py | 32 ++++- fastplotlib/layouts/_utils.py | 18 +++ .../ui/right_click_menus/_standard_menu.py | 5 +- 7 files changed, 144 insertions(+), 86 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index d485226f1..19544ec06 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -3,6 +3,7 @@ import numpy as np import pygfx +from ._utils import get_extents_from_grid from ._subplot import Subplot @@ -26,10 +27,12 @@ def _update_projection_matrix(self): class BaseLayout: def __init__( - self, renderer: pygfx.WgpuRenderer, subplots: list[Subplot], canvas_rect: tuple + self, + renderer: pygfx.WgpuRenderer, + subplots: np.ndarray[Subplot], canvas_rect: tuple, ): self._renderer = renderer - self._subplots = subplots + self._subplots = subplots.ravel() self._canvas_rect = canvas_rect def _inside_render_rect(self, subplot: Subplot, pos: tuple[int, int]) -> bool: @@ -57,16 +60,12 @@ def _fpl_canvas_resized(self, canvas_rect: tuple): for subplot in self._subplots: subplot._fpl_canvas_resized(canvas_rect) - @property - def spacing(self) -> int: - pass - def __len__(self): return len(self._subplots) class FlexLayout(BaseLayout): - def __init__(self, renderer, subplots: list[Subplot], canvas_rect: tuple): + def __init__(self, renderer, subplots: list[Subplot], canvas_rect: tuple, moveable=True, resizeable=True): super().__init__(renderer, subplots, canvas_rect) self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( @@ -76,22 +75,39 @@ def __init__(self, renderer, subplots: list[Subplot], canvas_rect: tuple): self._active_action: str | None = None self._active_subplot: Subplot | None = None - for frame in self._subplots: - frame._fpl_plane.add_event_handler( - partial(self._action_start, frame, "move"), "pointer_down" - ) - frame._fpl_resize_handle.add_event_handler( - partial(self._action_start, frame, "resize"), "pointer_down" - ) - frame._fpl_resize_handle.add_event_handler( - self._highlight_resize_handler, "pointer_enter" - ) - frame._fpl_resize_handle.add_event_handler( - self._unhighlight_resize_handler, "pointer_leave" - ) - - self._renderer.add_event_handler(self._action_iter, "pointer_move") - self._renderer.add_event_handler(self._action_end, "pointer_up") + for subplot in self._subplots: + if moveable: + # start a move action + subplot._fpl_plane.add_event_handler( + partial(self._action_start, subplot, "move"), "pointer_down" + ) + # start a resize action + subplot._fpl_resize_handle.add_event_handler( + partial(self._action_start, subplot, "resize"), "pointer_down" + ) + + # highlight plane when pointer enters + subplot._fpl_plane.add_event_handler( + partial(self._highlight_plane, subplot), "pointer_enter" + ) # unhighlight plane when pointer leaves + subplot._fpl_plane.add_event_handler( + partial(self._unhighlight_plane, subplot), "pointer_leave" + ) + if resizeable: + # highlight/unhighlight resize handler when pointer enters/leaves + subplot._fpl_resize_handle.add_event_handler( + partial(self._highlight_resize_handler, subplot), "pointer_enter" + ) + subplot._fpl_resize_handle.add_event_handler( + partial(self._unhighlight_resize_handler, subplot), "pointer_leave" + ) + + if moveable or resizeable: + # when pointer moves, do an iteration of move or resize action + self._renderer.add_event_handler(self._action_iter, "pointer_move") + + # end the action when pointer button goes up + self._renderer.add_event_handler(self._action_end, "pointer_up") def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: delta_x, delta_y = delta @@ -162,9 +178,9 @@ def _action_start(self, subplot: Subplot, action: str, ev): if ev.button == 1: self._active_action = action if action == "resize": - subplot._fpl_resize_handle.material.color = (1, 0, 1) + subplot._fpl_resize_handle.material.color = subplot.resize_handle_color.action elif action == "move": - pass + subplot._fpl_plane.material.color = subplot.plane_color.action else: raise ValueError @@ -183,40 +199,60 @@ def _action_iter(self, ev): def _action_end(self, ev): self._active_action = None if self._active_subplot is not None: - self._active_subplot._fpl_resize_handle.material.color = (0.5, 0.5, 0.5) + self._active_subplot._fpl_resize_handle.material.color = self._active_subplot.resize_handle_color.idle + self._active_subplot._fpl_plane.material.color = self._active_subplot.plane_color.idle self._active_subplot = None self._last_pointer_pos[:] = np.nan - def _highlight_resize_handler(self, ev): + def _highlight_resize_handler(self, subplot: Subplot, ev): if self._active_action == "resize": return - ev.target.material.color = (1, 1, 1) + ev.target.material.color = subplot.resize_handle_color.highlight - def _unhighlight_resize_handler(self, ev): + def _unhighlight_resize_handler(self, subplot: Subplot, ev): if self._active_action == "resize": return - ev.target.material.color = (0.5, 0.5, 0.5) + ev.target.material.color = subplot.resize_handle_color.idle + + def _highlight_plane(self, subplot: Subplot, ev): + if self._active_action is not None: + return + + ev.target.material.color = subplot.plane_color.highlight + + def _unhighlight_plane(self, subplot: Subplot, ev): + if self._active_action is not None: + return + + ev.target.material.color = subplot.plane_color.idle def add_subplot(self): pass - def remove_subplot(self): + def remove_subplot(self, subplot: Subplot): pass class GridLayout(FlexLayout): - def __init__(self, renderer, subplots: list[Subplot], canvas_rect: tuple): - super().__init__(renderer, subplots, canvas_rect) + def __init__( + self, + renderer, + subplots: list[Subplot], + canvas_rect: tuple[float, float, float, float], + shape: tuple[int, int] + ): + super().__init__(renderer, subplots, canvas_rect, moveable=False, resizeable=False) # {Subplot: (row_ix, col_ix)}, dict mapping subplots to their row and col index in the grid layout self._subplot_grid_position: dict[Subplot, tuple[int, int]] + self._shape = shape @property def shape(self) -> tuple[int, int]: - pass + return self._shape def set_rect(self, subplot, rect: np.ndarray | list | tuple): raise NotImplementedError( @@ -228,14 +264,12 @@ def set_extent(self, subplot, extent: np.ndarray | list | tuple): "set_extent() not implemented for GridLayout which is an auto layout manager" ) - def _fpl_set_subplot_viewport_rect(self): - pass - - def _fpl_set_subplot_dock_viewport_rect(self): - pass - def add_row(self): pass + # new_shape = (self.shape[0] + 1, self.shape[1]) + # extents = get_extents_from_grid(new_shape) + # for subplot, extent in zip(self._subplots, extents): + # subplot.extent = extent def add_column(self): pass @@ -245,3 +279,9 @@ def remove_row(self): def remove_column(self): pass + + def add_subplot(self): + raise NotImplementedError + + def remove_subplot(self, subplot): + raise NotImplementedError diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 6c790886e..73f0ae0fe 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -11,7 +11,7 @@ from rendercanvas import BaseRenderCanvas -from ._utils import make_canvas_and_renderer, create_controller, create_camera +from ._utils import make_canvas_and_renderer, create_controller, create_camera, get_extents_from_grid from ._utils import controller_types as valid_controller_types from ._subplot import Subplot from ._engine import GridLayout, FlexLayout, UnderlayCamera @@ -121,25 +121,13 @@ def __init__( "shape argument must be a list of bounding boxes or a tuple[n_rows, n_cols]" ) n_subplots = shape[0] * shape[1] - # shape is [n_subplots, row_col_index] - self._subplot_grid_positions: dict[Subplot, tuple[int, int]] = dict() layout_mode = "grid" # create fractional extents from the grid - x_mins = np.arange(0, 1, (1 / shape[0])) - x_maxs = x_mins + 1 / shape[0] - y_mins = np.arange(0, 1, (1 / shape[1])) - y_maxs = y_mins + 1 / shape[1] - - extents = np.column_stack([x_mins, x_maxs, y_mins, y_maxs]) + extents = get_extents_from_grid(shape) # empty rects rects = [None] * n_subplots - self._shape = shape - - # default spacing of 2 pixels between subplots - self._spacing = 2 - if names is not None: subplot_names = np.asarray(names).flatten() if subplot_names.size != n_subplots: @@ -332,16 +320,18 @@ def __init__( self._renderer = renderer if layout_mode == "grid": - n_rows, n_cols = self._shape + n_rows, n_cols = shape grid_index_iterator = list(product(range(n_rows), range(n_cols))) self._subplots: np.ndarray[Subplot] = np.empty( - shape=self._shape, dtype=object + shape=shape, dtype=object ) + resizeable = False else: self._subplots: np.ndarray[Subplot] = np.empty( shape=n_subplots, dtype=object ) + resizeable = True for i in range(n_subplots): camera = subplot_cameras[i] @@ -361,12 +351,12 @@ def __init__( name=name, rect=rects[i], extent=extents[i], # figure created extents for grid layout + resizeable=resizeable, ) if layout_mode == "grid": row_ix, col_ix = grid_index_iterator[i] self._subplots[row_ix, col_ix] = subplot - self._subplot_grid_positions[subplot] = (row_ix, col_ix) else: self._subplots[i] = subplot @@ -375,6 +365,7 @@ def __init__( self.renderer, subplots=self._subplots, canvas_rect=self.get_pygfx_render_area(), + shape=shape, ) elif layout_mode == "rect" or layout_mode == "extent": @@ -388,7 +379,7 @@ def __init__( self._underlay_scene = pygfx.Scene() - for subplot in self._subplots: + for subplot in self._subplots.ravel(): self._underlay_scene.add(subplot._world_object) self._animate_funcs_pre: list[callable] = list() @@ -415,20 +406,6 @@ def layout(self) -> FlexLayout | GridLayout: """ return self._layout - @property - def spacing(self) -> int: - """spacing between subplots, in pixels""" - return self._spacing - - @spacing.setter - def spacing(self, value: int): - """set the spacing between subplots, in pixels""" - if not isinstance(value, (int, np.integer)): - raise TypeError("spacing must be of type ") - - self._spacing = value - self._set_viewport_rects() - @property def canvas(self) -> BaseRenderCanvas: """The canvas this Figure is drawn onto""" @@ -901,14 +878,14 @@ def _set_viewport_rects(self, *ev): """set the viewport rects for all subplots, *ev argument is not used, exists because of renderer resize event""" self.layout._fpl_canvas_resized(self.get_pygfx_render_area()) - def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: + def get_pygfx_render_area(self, *args) -> tuple[float, float, float, float]: """ Fet rect for the portion of the canvas that the pygfx renderer draws to, i.e. non-imgui, part of canvas Returns ------- - tuple[int, int, int, int] + tuple[float, float, float, float] x_pos, y_pos, width, height """ @@ -930,13 +907,13 @@ def __len__(self): return len(self._layout) def __str__(self): - return f"{self.__class__.__name__} @ {hex(id(self))}" + return f"{self.__class__.__name__}" def __repr__(self): newline = "\n\t" return ( - f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}\n" + f"fastplotlib.{self.__class__.__name__}" f" Subplots:\n" f"\t{newline.join(subplot.__str__() for subplot in self)}" f"\n" diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index c4e6a9d70..f60f5149d 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -712,7 +712,7 @@ def __str__(self): else: name = self.name - return f"{name}: {self.__class__.__name__} @ {hex(id(self))}" + return f"{name}: {self.__class__.__name__}" def __repr__(self): newline = "\n\t" diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index a2073e35e..98d31ac86 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -156,9 +156,9 @@ def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): if (extent < 0).any(): raise ValueError(f"extent must be non-negative, you have passed: {extent}") - if extent[1] < 1 or extent[3] < 1: # if x1 < 1, or y1 < 1 + if extent[1] <= 1 or extent[3] <= 1: # if x1 <= 1, or y1 <= 1 # if fractional rect, convert to full - if not (extent < 1).all(): # if x1 and y1 < 1, then all vals must be < 1 + if not (extent <= 1).all(): # if x1 and y1 <= 1, then all vals must be <= 1 raise ValueError( f"if passing a fractional extent, all values must be fractional, you have passed: {extent}" ) diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index ae0cbaa2e..33a54e610 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -11,6 +11,7 @@ from ._plot_area import PlotArea from ._graphic_methods_mixin import GraphicMethodsMixin from ..graphics._axes import Axes +from ..utils._types import SelectorColorStates """ @@ -85,6 +86,18 @@ class MeshMasks: class Subplot(PlotArea, GraphicMethodsMixin): + resize_handle_color = SelectorColorStates( + idle=(0.5, 0.5, 0.5, 1), # gray + highlight=(1, 1, 1, 1), # white + action=(1, 1, 0, 1) # yellow + ) + + plane_color = SelectorColorStates( + idle=(0.1, 0.1, 0.1), # dark grey + highlight=(0.2, 0.2, 0.2), # less dark grey + action=(0.1, 0.1, 0.2) # dark gray-blue + ) + def __init__( self, parent: Union["Figure"], @@ -93,6 +106,7 @@ def __init__( canvas: BaseRenderCanvas | pygfx.Texture, rect: np.ndarray = None, extent: np.ndarray = None, + resizeable: bool = True, renderer: pygfx.WgpuRenderer = None, name: str = None, ): @@ -172,17 +186,20 @@ def __init__( else: raise ValueError("Must provide `rect` or `extent`") + wobjects = list() + if name is None: title_text = "" else: title_text = name self._title_graphic = TextGraphic(title_text, font_size=16, face_color="white") - # self._title_graphic.world_object.material.weight_offset = 50 + wobjects.append(self._title_graphic.world_object) # init mesh of size 1 to graphically represent rect geometry = pygfx.plane_geometry(1, 1) material = pygfx.MeshBasicMaterial(color=(0.1, 0.1, 0.1), pick_write=True) self._plane = pygfx.Mesh(geometry, material) + wobjects.append(self._plane) # otherwise text isn't visible self._plane.world.z = 0.5 @@ -192,17 +209,22 @@ def __init__( self._resize_handle = pygfx.Points( pygfx.Geometry(positions=[[x1, -y1, 0]]), # y is inverted in UnderlayCamera pygfx.PointsMarkerMaterial( - color=(0.5, 0.5, 0.5), marker="square", size=8, size_space="screen", pick_write=True + color=(0.5, 0.5, 0.5, 1), marker="square", size=8, size_space="screen", pick_write=True ), ) + if not resizeable: + c = (0, 0, 0, 0) + self._resize_handle.material.color = c + self._resize_handle.material.edge_width = 0 + self.resize_handle_color = SelectorColorStates(c, c, c) + + wobjects.append(self._resize_handle) self._reset_plane() self._reset_viewport_rect() self._world_object = pygfx.Group() - self._world_object.add( - self._plane, self._resize_handle, self._title_graphic.world_object - ) + self._world_object.add(*wobjects) @property def axes(self) -> Axes: diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 3f8d43f6a..f9af38712 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -1,4 +1,7 @@ import importlib +from itertools import product + +import numpy as np import pygfx from pygfx import WgpuRenderer, Texture, Renderer @@ -103,3 +106,18 @@ def create_controller( ) return controller_types[controller_type](camera) + + +def get_extents_from_grid(shape: tuple[int, int]) -> list[tuple[float, float, float, float]]: + """create fractional extents from a given grid shape""" + x_min = np.arange(0, 1, (1 / shape[1])) + x_max = x_min + 1 / shape[1] + y_min = np.arange(0, 1, (1 / shape[0])) + y_max = y_min + 1 / shape[0] + + extents = list() + for row_ix, col_ix in product(range(shape[0]), range(shape[1])): + extent = x_min[col_ix], x_max[col_ix], y_min[row_ix], y_max[row_ix] + extents.append(extent) + + return extents diff --git a/fastplotlib/ui/right_click_menus/_standard_menu.py b/fastplotlib/ui/right_click_menus/_standard_menu.py index 772baa170..0a7fbd619 100644 --- a/fastplotlib/ui/right_click_menus/_standard_menu.py +++ b/fastplotlib/ui/right_click_menus/_standard_menu.py @@ -55,8 +55,9 @@ def update(self): # open popup only if mouse was not moved between mouse_down and mouse_up events if self._last_right_click_pos == imgui.get_mouse_pos(): - if self.get_subplot(): + if self.get_subplot() is not False: # must explicitly check for False # open only if right click was inside a subplot + print("opening right click menu") imgui.open_popup(f"right-click-menu") # TODO: call this just once when going from open -> closed state @@ -64,7 +65,7 @@ def update(self): self.cleanup() if imgui.begin_popup(f"right-click-menu"): - if not self.get_subplot(): + if self.get_subplot() is False: # must explicitly check for False # for some reason it will still trigger at certain locations # despite open_popup() only being called when an actual # subplot is returned From 0989157efcf43aceffcad079a511b58504f5a0a9 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 00:32:47 -0500 Subject: [PATCH 26/82] add or remove subplot, not tested --- fastplotlib/layouts/_engine.py | 7 -- fastplotlib/layouts/_figure.py | 205 ++++++++------------------------- 2 files changed, 45 insertions(+), 167 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 19544ec06..4c39a5b00 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -3,7 +3,6 @@ import numpy as np import pygfx -from ._utils import get_extents_from_grid from ._subplot import Subplot @@ -229,12 +228,6 @@ def _unhighlight_plane(self, subplot: Subplot, ev): ev.target.material.color = subplot.plane_color.idle - def add_subplot(self): - pass - - def remove_subplot(self, subplot: Subplot): - pass - class GridLayout(FlexLayout): def __init__( diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 73f0ae0fe..e3388cea0 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -449,18 +449,6 @@ def names(self) -> np.ndarray[str]: names.flags.writeable = False return names - def __getitem__(self, index: str | int | tuple[int, int]) -> Subplot: - if isinstance(index, str): - for subplot in self._subplots.ravel(): - if subplot.name == index: - return subplot - raise IndexError(f"no subplot with given name: {index}") - - if isinstance(self.layout, GridLayout): - return self._subplots[index[0], index[1]] - - return self._subplots[index] - def _render(self, draw=True): # draw the underlay planes self.renderer.render(self._underlay_scene, self._underlay_camera, flush=False) @@ -726,154 +714,6 @@ def export(self, uri: str | Path | bytes, **kwargs): def open_popup(self, *args, **kwargs): warn("popups only supported by ImguiFigure") - # def _fpl_set_subplot_viewport_rect(self, subplot: Subplot): - # """ - # Sets the viewport rect for the given subplot - # """ - # - # if self.mode == "grid": - # # row, col position of this subplot within the grid - # row_ix, col_ix = self._subplot_grid_positions[subplot] - # - # # number of rows, cols in the grid - # nrows, ncols = self.shape - # - # # get starting positions and dimensions for the pygfx portion of the canvas - # # anything outside the pygfx portion of the canvas is for imgui - # x0_canvas, y0_canvas, width_canvas, height_canvas = ( - # self.get_pygfx_render_area() - # ) - # - # # width of an individual subplot - # width_subplot = width_canvas / ncols - # # height of an individual subplot - # height_subplot = height_canvas / nrows - # - # # x position of this subplot - # x_pos = ( - # ((col_ix - 1) * width_subplot) - # + width_subplot - # + x0_canvas - # + self.spacing - # ) - # # y position of this subplot - # y_pos = ( - # ((row_ix - 1) * height_subplot) - # + height_subplot - # + y0_canvas - # + self.spacing - # ) - # - # if self.__class__.__name__ == "ImguiFigure" and subplot.toolbar: - # # leave space for imgui toolbar - # height_subplot -= IMGUI_TOOLBAR_HEIGHT - # - # # clip so that min (w, h) is always 1, otherwise JupyterRenderCanvas causes issues because it - # # initializes with a width, height of (0, 0) - # rect = np.array( - # [ - # x_pos, - # y_pos, - # width_subplot - self.spacing, - # height_subplot - self.spacing, - # ] - # ).clip(min=[0, 0, 1, 1]) - # - # # adjust if a subplot dock is present - # adjust = np.array( - # [ - # # add left dock size to x_pos - # subplot.docks["left"].size, - # # add top dock size to y_pos - # subplot.docks["top"].size, - # # remove left and right dock sizes from width - # -subplot.docks["right"].size - subplot.docks["left"].size, - # # remove top and bottom dock sizes from height - # -subplot.docks["top"].size - subplot.docks["bottom"].size, - # ] - # ) - # - # subplot.viewport.rect = rect + adjust - - # def _fpl_set_subplot_dock_viewport_rect(self, subplot, position): - # """ - # Sets the viewport rect for the given subplot dock - # """ - # - # dock = subplot.docks[position] - # - # if dock.size == 0: - # dock.viewport.rect = None - # return - # - # if self.mode == "grid": - # # row, col position of this subplot within the grid - # row_ix, col_ix = self._subplot_grid_positions[subplot] - # - # # number of rows, cols in the grid - # nrows, ncols = self.shape - # - # x0_canvas, y0_canvas, width_canvas, height_canvas = ( - # self.get_pygfx_render_area() - # ) - # - # # width of an individual subplot - # width_subplot = width_canvas / ncols - # # height of an individual subplot - # height_subplot = height_canvas / nrows - # - # # calculate the rect based on the dock position - # match position: - # case "right": - # x_pos = ( - # ((col_ix - 1) * width_subplot) + (width_subplot * 2) - dock.size - # ) - # y_pos = ( - # ((row_ix - 1) * height_subplot) + height_subplot + self.spacing - # ) - # width_viewport = dock.size - # height_viewport = height_subplot - self.spacing - # - # case "left": - # x_pos = ((col_ix - 1) * width_subplot) + width_subplot - # y_pos = ( - # ((row_ix - 1) * height_subplot) + height_subplot + self.spacing - # ) - # width_viewport = dock.size - # height_viewport = height_subplot - self.spacing - # - # case "top": - # x_pos = ( - # ((col_ix - 1) * width_subplot) + width_subplot + self.spacing - # ) - # y_pos = ( - # ((row_ix - 1) * height_subplot) + height_subplot + self.spacing - # ) - # width_viewport = width_subplot - self.spacing - # height_viewport = dock.size - # - # case "bottom": - # x_pos = ( - # ((col_ix - 1) * width_subplot) + width_subplot + self.spacing - # ) - # y_pos = ( - # ((row_ix - 1) * height_subplot) - # + (height_subplot * 2) - # - dock.size - # ) - # width_viewport = width_subplot - self.spacing - # height_viewport = dock.size - # - # case _: - # raise ValueError("invalid position") - # - # dock.viewport.rect = [ - # x_pos + x0_canvas, - # y_pos + y0_canvas, - # width_viewport, - # height_viewport, - # ] - def _set_viewport_rects(self, *ev): """set the viewport rects for all subplots, *ev argument is not used, exists because of renderer resize event""" self.layout._fpl_canvas_resized(self.get_pygfx_render_area()) @@ -894,6 +734,51 @@ def get_pygfx_render_area(self, *args) -> tuple[float, float, float, float]: return 0, 0, width, height + def add_subplot(self, rect=None, extent=None, camera: str | pygfx.PerspectiveCamera = "2d", controller: str | pygfx.Controller = None, name: str = None) -> Subplot: + if isinstance(self.layout, GridLayout): + raise NotImplementedError("`add_subplot()` is not implemented for Figures using a GridLayout") + + camera = create_camera(camera) + controller = create_controller(controller, camera) + + subplot = Subplot( + parent=self, + camera=camera, + controller=controller, + canvas=self.canvas, + renderer=self.renderer, + name=name, + rect=rect, + extent=extent, # figure created extents for grid layout + resizeable=True, + ) + + return subplot + + def remove_subplot(self, subplot: Subplot): + if isinstance(self.layout, GridLayout): + raise NotImplementedError("`remove_subplot()` is not implemented for Figures using a GridLayout") + + if subplot not in self._subplots.tolist(): + raise KeyError(f"given subplot: {subplot} not found in the layout.") + + subplot.clear() + self._underlay_scene.remove(subplot._world_object) + subplot._world_object.clear() + del subplot + + def __getitem__(self, index: str | int | tuple[int, int]) -> Subplot: + if isinstance(index, str): + for subplot in self._subplots.ravel(): + if subplot.name == index: + return subplot + raise IndexError(f"no subplot with given name: {index}") + + if isinstance(self.layout, GridLayout): + return self._subplots[index[0], index[1]] + + return self._subplots[index] + def __iter__(self): self._current_iter = iter(range(len(self))) return self From f19c2941d40248e55f9c618ab6bbe0b8e677a563 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 01:04:15 -0500 Subject: [PATCH 27/82] better highlight behavior --- fastplotlib/layouts/_engine.py | 37 +++++++++++++++++++++------------- fastplotlib/layouts/_figure.py | 14 ++++++++++++- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 4c39a5b00..d8b2bbdb6 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -48,6 +48,12 @@ def _inside_render_rect(self, subplot: Subplot, pos: tuple[int, int]) -> bool: return False + def add_subplot(self): + raise NotImplementedError + + def remove_subplot(self, subplot): + raise NotImplementedError + def set_rect(self, subplot, rect: np.ndarray | list | tuple): raise NotImplementedError @@ -73,8 +79,14 @@ def __init__(self, renderer, subplots: list[Subplot], canvas_rect: tuple, moveab self._active_action: str | None = None self._active_subplot: Subplot | None = None + self._subplot_focus: Subplot | None = None for subplot in self._subplots: + # highlight plane when pointer enters it + subplot._fpl_plane.add_event_handler( + partial(self._highlight_plane, subplot), "pointer_enter" + ) + if moveable: # start a move action subplot._fpl_plane.add_event_handler( @@ -85,13 +97,6 @@ def __init__(self, renderer, subplots: list[Subplot], canvas_rect: tuple, moveab partial(self._action_start, subplot, "resize"), "pointer_down" ) - # highlight plane when pointer enters - subplot._fpl_plane.add_event_handler( - partial(self._highlight_plane, subplot), "pointer_enter" - ) # unhighlight plane when pointer leaves - subplot._fpl_plane.add_event_handler( - partial(self._unhighlight_plane, subplot), "pointer_leave" - ) if resizeable: # highlight/unhighlight resize handler when pointer enters/leaves subplot._fpl_resize_handle.add_event_handler( @@ -108,6 +113,12 @@ def __init__(self, renderer, subplots: list[Subplot], canvas_rect: tuple, moveab # end the action when pointer button goes up self._renderer.add_event_handler(self._action_end, "pointer_up") + def remove_subplot(self, subplot): + if subplot is self._active_subplot: + self._active_subplot = None + if subplot is self._subplot_focus: + self._subplot_focus = None + def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: delta_x, delta_y = delta if self._active_action == "resize": @@ -220,14 +231,12 @@ def _highlight_plane(self, subplot: Subplot, ev): if self._active_action is not None: return - ev.target.material.color = subplot.plane_color.highlight - - def _unhighlight_plane(self, subplot: Subplot, ev): - if self._active_action is not None: - return - - ev.target.material.color = subplot.plane_color.idle + # reset color of previous focus + if self._subplot_focus is not None: + self._subplot_focus._fpl_plane.material.color = subplot.plane_color.idle + self._subplot_focus = subplot + ev.target.material.color = subplot.plane_color.highlight class GridLayout(FlexLayout): def __init__( diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index e3388cea0..1bcfc89e7 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -135,7 +135,12 @@ def __init__( "must provide same number of subplot `names` as specified by Figure `shape`" ) else: - subplot_names = None + if layout_mode == "grid": + subplot_names = np.asarray( + list(map(str, product(range(shape[0]), range(shape[1])))) + ) + else: + subplot_names = None canvas, renderer = make_canvas_and_renderer( canvas, renderer, canvas_kwargs={"size": size} @@ -765,8 +770,15 @@ def remove_subplot(self, subplot: Subplot): subplot.clear() self._underlay_scene.remove(subplot._world_object) subplot._world_object.clear() + self.layout._subplots = None + subplots = self._subplots.tolist() + subplots.remove(subplot) + self.layout.remove_subplot(subplot) del subplot + self._subplots = np.asarray(subplots) + self.layout._subplots = self._subplots.ravel() + def __getitem__(self, index: str | int | tuple[int, int]) -> Subplot: if isinstance(index, str): for subplot in self._subplots.ravel(): From 09a70365ad00f696600f7967d7caf22c8e2c1fa5 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 01:06:07 -0500 Subject: [PATCH 28/82] increase size of example fig --- examples/image_widget/image_widget_videos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/image_widget/image_widget_videos.py b/examples/image_widget/image_widget_videos.py index 1e367f0ad..7de4a9c04 100644 --- a/examples/image_widget/image_widget_videos.py +++ b/examples/image_widget/image_widget_videos.py @@ -29,7 +29,7 @@ [random_data, cockatoo_sub], rgb=[False, True], figure_shape=(2, 1), # 2 rows, 1 column - figure_kwargs={"size": (700, 560)} + figure_kwargs={"size": (700, 940)} ) iw.show() From da6626048af5793e6842808b2b4c6befa46a1c38 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 01:06:35 -0500 Subject: [PATCH 29/82] repr --- fastplotlib/graphics/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index a25bc7176..61ad291ee 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -365,7 +365,7 @@ def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area def __repr__(self): - rval = f"{self.__class__.__name__} @ {hex(id(self))}" + rval = f"{self.__class__.__name__}" if self.name is not None: return f"'{self.name}': {rval}" else: From 13a40496c7aa1b942110b60c0566182b8a4dfb95 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 02:38:55 -0500 Subject: [PATCH 30/82] sdf for resize handle, better resize handle, overlap stuff with distance --- fastplotlib/layouts/_subplot.py | 48 +++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 33a54e610..bc02e021d 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -41,6 +41,18 @@ """ +# wgsl shader snipper for SDF function that defines the resize handler, a lower right triangle. +sdf_wgsl_resize_handler = """ +// hardcode square root of 2 +let m_sqrt_2 = 1.4142135; + +// given a distance from an origin point, this defines the hypotenuse of a lower right triangle +let distance = (-coord.x + coord.y) / m_sqrt_2; + +// return distance for this position +return distance * size; +""" + class MeshMasks: """Used set the x1, x1, y0, y1 positions of the mesh""" @@ -87,9 +99,9 @@ class MeshMasks: class Subplot(PlotArea, GraphicMethodsMixin): resize_handle_color = SelectorColorStates( - idle=(0.5, 0.5, 0.5, 1), # gray + idle=(0.6, 0.6, 0.6, 1), # gray highlight=(1, 1, 1, 1), # white - action=(1, 1, 0, 1) # yellow + action=(1, 0, 1, 1) # magenta ) plane_color = SelectorColorStates( @@ -197,7 +209,7 @@ def __init__( # init mesh of size 1 to graphically represent rect geometry = pygfx.plane_geometry(1, 1) - material = pygfx.MeshBasicMaterial(color=(0.1, 0.1, 0.1), pick_write=True) + material = pygfx.MeshBasicMaterial(color=self.plane_color.idle, pick_write=True) self._plane = pygfx.Mesh(geometry, material) wobjects.append(self._plane) @@ -207,9 +219,9 @@ def __init__( # create resize handler at point (x1, y1) x1, y1 = self.extent[[1, 3]] self._resize_handle = pygfx.Points( - pygfx.Geometry(positions=[[x1, -y1, 0]]), # y is inverted in UnderlayCamera + pygfx.Geometry(positions=[[x1 - 7, -y1 + 7, 0]]), # y is inverted in UnderlayCamera pygfx.PointsMarkerMaterial( - color=(0.5, 0.5, 0.5, 1), marker="square", size=8, size_space="screen", pick_write=True + color=self.resize_handle_color.idle, marker="custom", custom_sdf=sdf_wgsl_resize_handler, size=12, size_space="screen", pick_write=True ), ) if not resizeable: @@ -358,8 +370,9 @@ def _fpl_get_render_rect(self) -> tuple[float, float, float, float]: else: toolbar_space = 0 - # adjust for spacing and 4 pixels for more spacing - h = h - 4 - self.title.font_size - toolbar_space - 4 - 4 + # adjust for the 4 pixels from the line above + # also adjust for toolbar space and 13 pixels for the resize handler + h = h - 4 - self.title.font_size - toolbar_space - 4 - 13 return x, y, w, h @@ -379,7 +392,8 @@ def _reset_plane(self): self._plane.geometry.positions.update_full() # note the negative y because UnderlayCamera y is inverted - self._resize_handle.geometry.positions.data[0] = [x1, -y1, 0] + # shifted by 8 so lower right of triangle is at the edge of the subplot plane + self._resize_handle.geometry.positions.data[0] = [x1 - 7, -y1 + 7, 0] self._resize_handle.geometry.positions.update_full() # set subplot title position @@ -404,22 +418,22 @@ def _fpl_canvas_resized(self, canvas_rect): self._reset_plane() self._reset_viewport_rect() - def is_above(self, y0) -> bool: - # our bottom < other top - return self._rect.y1 < y0 + def is_above(self, y0, dist: int = 1) -> bool: + # our bottom < other top within given distance + return self._rect.y1 < y0 + dist - def is_below(self, y1) -> bool: + def is_below(self, y1, dist: int = 1) -> bool: # our top > other bottom - return self._rect.y0 > y1 + return self._rect.y0 > y1 - dist - def is_left_of(self, x0) -> bool: + def is_left_of(self, x0, dist: int = 1) -> bool: # our right_edge < other left_edge # self.x1 < other.x0 - return self._rect.x1 < x0 + return self._rect.x1 < x0 + dist - def is_right_of(self, x1) -> bool: + def is_right_of(self, x1, dist: int = 1) -> bool: # self.x0 > other.x1 - return self._rect.x0 > x1 + return self._rect.x0 > x1 - dist def overlaps(self, extent: np.ndarray) -> bool: """returns whether this subplot overlaps with the given extent""" From 8f52f503628f48ce2e05b88369c78267a1077144 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 02:39:20 -0500 Subject: [PATCH 31/82] cleanup --- fastplotlib/layouts/_engine.py | 3 ++- fastplotlib/layouts/_rect.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index d8b2bbdb6..19ceffa42 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -31,7 +31,7 @@ def __init__( subplots: np.ndarray[Subplot], canvas_rect: tuple, ): self._renderer = renderer - self._subplots = subplots.ravel() + self._subplots: np.ndarray[Subplot] = subplots.ravel() self._canvas_rect = canvas_rect def _inside_render_rect(self, subplot: Subplot, pos: tuple[int, int]) -> bool: @@ -238,6 +238,7 @@ def _highlight_plane(self, subplot: Subplot, ev): self._subplot_focus = subplot ev.target.material.color = subplot.plane_color.highlight + class GridLayout(FlexLayout): def __init__( self, diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index 98d31ac86..481d69d8b 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -143,9 +143,7 @@ def extent_to_rect(extent, canvas_rect): w = x1 - x0 h = y1 - y0 - x, y, w, h = x0, y0, w, h - - return x, y, w, h + return x0, y0, w, h @staticmethod def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): From cf0fa1fb55eb8f24a77e95a56001af2157f26862 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 02:48:50 -0500 Subject: [PATCH 32/82] more space --- fastplotlib/ui/_subplot_toolbar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/ui/_subplot_toolbar.py b/fastplotlib/ui/_subplot_toolbar.py index cebd916e8..aa599838c 100644 --- a/fastplotlib/ui/_subplot_toolbar.py +++ b/fastplotlib/ui/_subplot_toolbar.py @@ -22,7 +22,7 @@ def update(self): # place the toolbar window below the subplot pos = (x + 1, y + height - IMGUI_TOOLBAR_HEIGHT) - imgui.set_next_window_size((width - 16, 0)) + imgui.set_next_window_size((width - 18, 0)) imgui.set_next_window_pos(pos) flags = ( imgui.WindowFlags_.no_collapse From 8ad30861aa9ede271f7b11857c7f750541d71e4e Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 02:51:43 -0500 Subject: [PATCH 33/82] spacing tweaks --- fastplotlib/layouts/_subplot.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index bc02e021d..b6496bf2a 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -41,7 +41,7 @@ """ -# wgsl shader snipper for SDF function that defines the resize handler, a lower right triangle. +# wgsl shader snippet for SDF function that defines the resize handler, a lower right triangle. sdf_wgsl_resize_handler = """ // hardcode square root of 2 let m_sqrt_2 = 1.4142135; @@ -219,7 +219,9 @@ def __init__( # create resize handler at point (x1, y1) x1, y1 = self.extent[[1, 3]] self._resize_handle = pygfx.Points( - pygfx.Geometry(positions=[[x1 - 7, -y1 + 7, 0]]), # y is inverted in UnderlayCamera + # note negative y since y is inverted in UnderlayCamera + # subtract 7 so that the bottom right corner of the triangle is at the center + pygfx.Geometry(positions=[[x1 - 7, -y1 + 7, 0]]), pygfx.PointsMarkerMaterial( color=self.resize_handle_color.idle, marker="custom", custom_sdf=sdf_wgsl_resize_handler, size=12, size_space="screen", pick_write=True ), @@ -367,12 +369,15 @@ def _fpl_get_render_rect(self) -> tuple[float, float, float, float]: if self.toolbar: toolbar_space = IMGUI_TOOLBAR_HEIGHT + resize_handle_space = 0 else: toolbar_space = 0 + # need some space for resize handler if imgui toolbar isn't present + resize_handle_space = 13 # adjust for the 4 pixels from the line above - # also adjust for toolbar space and 13 pixels for the resize handler - h = h - 4 - self.title.font_size - toolbar_space - 4 - 13 + # also give space for resize handler if imgui toolbar is not present + h = h - 4 - self.title.font_size - toolbar_space - 4 - resize_handle_space return x, y, w, h @@ -391,8 +396,8 @@ def _reset_plane(self): self._plane.geometry.positions.update_full() - # note the negative y because UnderlayCamera y is inverted - # shifted by 8 so lower right of triangle is at the edge of the subplot plane + # note negative y since y is inverted in UnderlayCamera + # subtract 7 so that the bottom right corner of the triangle is at the center self._resize_handle.geometry.positions.data[0] = [x1 - 7, -y1 + 7, 0] self._resize_handle.geometry.positions.update_full() From 88df692b97c330d62f1234e54d20ac65b482b37a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 02:52:00 -0500 Subject: [PATCH 34/82] add utils._types --- fastplotlib/utils/_types.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 fastplotlib/utils/_types.py diff --git a/fastplotlib/utils/_types.py b/fastplotlib/utils/_types.py new file mode 100644 index 000000000..e99fce2fc --- /dev/null +++ b/fastplotlib/utils/_types.py @@ -0,0 +1,4 @@ +from collections import namedtuple + + +SelectorColorStates = namedtuple("state", ["idle", "highlight", "action"]) From f94e18a2c19afefaf9bf4834536683e84d8b3ec5 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 02:58:55 -0500 Subject: [PATCH 35/82] unit circle using extents --- examples/selection_tools/unit_circle.py | 30 ++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/examples/selection_tools/unit_circle.py b/examples/selection_tools/unit_circle.py index 76f6a207c..2850b1bc1 100644 --- a/examples/selection_tools/unit_circle.py +++ b/examples/selection_tools/unit_circle.py @@ -28,12 +28,39 @@ def make_circle(center, radius: float, n_points: int) -> np.ndarray: return np.column_stack([xs, ys]) + center +# We will have 3 subplots in a layout like this: +""" +|========|========| +| | | +| | sine | +| | | +| circle |========| +| | | +| | cosine | +| | | +|========|========| +""" + +# we can define this layout using "extents", i.e. min and max ranges on the canvas +# (x_min, x_max, y_min, y_max) +# extents can be defined as fractions as shown here +extents = [ + (0, 0.5, 0, 1), # circle subplot + (0.5, 1, 0, 0.5), # sine subplot + (0.5, 1, 0.5, 1), # cosine subplot +] + # create a figure with 3 subplots -figure = fpl.Figure((3, 1), names=["unit circle", "sin(x)", "cos(x)"], size=(700, 1024)) +figure = fpl.Figure( + extents=extents, + names=["unit circle", "sin(x)", "cos(x)"], + size=(700, 560) +) # set the axes to intersect at (0, 0, 0) to better illustrate the unit circle for subplot in figure: subplot.axes.intersection = (0, 0, 0) + subplot.toolbar = False # reduce clutter figure["sin(x)"].camera.maintain_aspect = False figure["cos(x)"].camera.maintain_aspect = False @@ -73,6 +100,7 @@ def make_circle(center, radius: float, n_points: int) -> np.ndarray: sine_selector = sine_graphic.add_linear_selector() cosine_selector = cosine_graphic.add_linear_selector() + def set_circle_cmap(ev): # sets the cmap transforms From cb3d3ba138ded720fd7b98517c7d47fe9a332615 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sat, 8 Mar 2025 03:33:10 -0500 Subject: [PATCH 36/82] black --- fastplotlib/layouts/_engine.py | 38 +++++++++++++++++++++++---------- fastplotlib/layouts/_figure.py | 28 ++++++++++++++++++------ fastplotlib/layouts/_subplot.py | 22 ++++++++++++++----- fastplotlib/layouts/_utils.py | 4 +++- 4 files changed, 68 insertions(+), 24 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 19ceffa42..ab2188d1d 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -28,7 +28,8 @@ class BaseLayout: def __init__( self, renderer: pygfx.WgpuRenderer, - subplots: np.ndarray[Subplot], canvas_rect: tuple, + subplots: np.ndarray[Subplot], + canvas_rect: tuple, ): self._renderer = renderer self._subplots: np.ndarray[Subplot] = subplots.ravel() @@ -70,7 +71,14 @@ def __len__(self): class FlexLayout(BaseLayout): - def __init__(self, renderer, subplots: list[Subplot], canvas_rect: tuple, moveable=True, resizeable=True): + def __init__( + self, + renderer, + subplots: list[Subplot], + canvas_rect: tuple, + moveable=True, + resizeable=True, + ): super().__init__(renderer, subplots, canvas_rect) self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( @@ -188,7 +196,9 @@ def _action_start(self, subplot: Subplot, action: str, ev): if ev.button == 1: self._active_action = action if action == "resize": - subplot._fpl_resize_handle.material.color = subplot.resize_handle_color.action + subplot._fpl_resize_handle.material.color = ( + subplot.resize_handle_color.action + ) elif action == "move": subplot._fpl_plane.material.color = subplot.plane_color.action else: @@ -209,8 +219,12 @@ def _action_iter(self, ev): def _action_end(self, ev): self._active_action = None if self._active_subplot is not None: - self._active_subplot._fpl_resize_handle.material.color = self._active_subplot.resize_handle_color.idle - self._active_subplot._fpl_plane.material.color = self._active_subplot.plane_color.idle + self._active_subplot._fpl_resize_handle.material.color = ( + self._active_subplot.resize_handle_color.idle + ) + self._active_subplot._fpl_plane.material.color = ( + self._active_subplot.plane_color.idle + ) self._active_subplot = None self._last_pointer_pos[:] = np.nan @@ -241,13 +255,15 @@ def _highlight_plane(self, subplot: Subplot, ev): class GridLayout(FlexLayout): def __init__( - self, - renderer, - subplots: list[Subplot], - canvas_rect: tuple[float, float, float, float], - shape: tuple[int, int] + self, + renderer, + subplots: list[Subplot], + canvas_rect: tuple[float, float, float, float], + shape: tuple[int, int], ): - super().__init__(renderer, subplots, canvas_rect, moveable=False, resizeable=False) + super().__init__( + renderer, subplots, canvas_rect, moveable=False, resizeable=False + ) # {Subplot: (row_ix, col_ix)}, dict mapping subplots to their row and col index in the grid layout self._subplot_grid_position: dict[Subplot, tuple[int, int]] diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 1bcfc89e7..95b64b0da 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -11,7 +11,12 @@ from rendercanvas import BaseRenderCanvas -from ._utils import make_canvas_and_renderer, create_controller, create_camera, get_extents_from_grid +from ._utils import ( + make_canvas_and_renderer, + create_controller, + create_camera, + get_extents_from_grid, +) from ._utils import controller_types as valid_controller_types from ._subplot import Subplot from ._engine import GridLayout, FlexLayout, UnderlayCamera @@ -327,9 +332,7 @@ def __init__( if layout_mode == "grid": n_rows, n_cols = shape grid_index_iterator = list(product(range(n_rows), range(n_cols))) - self._subplots: np.ndarray[Subplot] = np.empty( - shape=shape, dtype=object - ) + self._subplots: np.ndarray[Subplot] = np.empty(shape=shape, dtype=object) resizeable = False else: @@ -739,9 +742,18 @@ def get_pygfx_render_area(self, *args) -> tuple[float, float, float, float]: return 0, 0, width, height - def add_subplot(self, rect=None, extent=None, camera: str | pygfx.PerspectiveCamera = "2d", controller: str | pygfx.Controller = None, name: str = None) -> Subplot: + def add_subplot( + self, + rect=None, + extent=None, + camera: str | pygfx.PerspectiveCamera = "2d", + controller: str | pygfx.Controller = None, + name: str = None, + ) -> Subplot: if isinstance(self.layout, GridLayout): - raise NotImplementedError("`add_subplot()` is not implemented for Figures using a GridLayout") + raise NotImplementedError( + "`add_subplot()` is not implemented for Figures using a GridLayout" + ) camera = create_camera(camera) controller = create_controller(controller, camera) @@ -762,7 +774,9 @@ def add_subplot(self, rect=None, extent=None, camera: str | pygfx.PerspectiveCam def remove_subplot(self, subplot: Subplot): if isinstance(self.layout, GridLayout): - raise NotImplementedError("`remove_subplot()` is not implemented for Figures using a GridLayout") + raise NotImplementedError( + "`remove_subplot()` is not implemented for Figures using a GridLayout" + ) if subplot not in self._subplots.tolist(): raise KeyError(f"given subplot: {subplot} not found in the layout.") diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index b6496bf2a..73ff0608a 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -101,13 +101,13 @@ class Subplot(PlotArea, GraphicMethodsMixin): resize_handle_color = SelectorColorStates( idle=(0.6, 0.6, 0.6, 1), # gray highlight=(1, 1, 1, 1), # white - action=(1, 0, 1, 1) # magenta + action=(1, 0, 1, 1), # magenta ) plane_color = SelectorColorStates( idle=(0.1, 0.1, 0.1), # dark grey highlight=(0.2, 0.2, 0.2), # less dark grey - action=(0.1, 0.1, 0.2) # dark gray-blue + action=(0.1, 0.1, 0.2), # dark gray-blue ) def __init__( @@ -223,7 +223,12 @@ def __init__( # subtract 7 so that the bottom right corner of the triangle is at the center pygfx.Geometry(positions=[[x1 - 7, -y1 + 7, 0]]), pygfx.PointsMarkerMaterial( - color=self.resize_handle_color.idle, marker="custom", custom_sdf=sdf_wgsl_resize_handler, size=12, size_space="screen", pick_write=True + color=self.resize_handle_color.idle, + marker="custom", + custom_sdf=sdf_wgsl_resize_handler, + size=12, + size_space="screen", + pick_write=True, ), ) if not resizeable: @@ -342,7 +347,12 @@ def _reset_viewport_rect(self): # set dock rects self.docks["left"].viewport.rect = x, y, s_left, h self.docks["top"].viewport.rect = x_top_bottom, y, w_top_bottom, s_top - self.docks["bottom"].viewport.rect = x_top_bottom, y + h - s_bottom, w_top_bottom, s_bottom + self.docks["bottom"].viewport.rect = ( + x_top_bottom, + y + h - s_bottom, + w_top_bottom, + s_bottom, + ) self.docks["right"].viewport.rect = x + w - s_right, y, s_right, h # calc subplot rect by adjusting for dock sizes @@ -365,7 +375,9 @@ def _fpl_get_render_rect(self) -> tuple[float, float, float, float]: x += 1 # add 1 so a 1 pixel edge is visible w -= 2 # subtract 2, so we get a 1 pixel edge on both sides - y = y + 4 + self.title.font_size + 4 # add 4 pixels above and below title for better spacing + y = ( + y + 4 + self.title.font_size + 4 + ) # add 4 pixels above and below title for better spacing if self.toolbar: toolbar_space = IMGUI_TOOLBAR_HEIGHT diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index f9af38712..beec8dd39 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -108,7 +108,9 @@ def create_controller( return controller_types[controller_type](camera) -def get_extents_from_grid(shape: tuple[int, int]) -> list[tuple[float, float, float, float]]: +def get_extents_from_grid( + shape: tuple[int, int] +) -> list[tuple[float, float, float, float]]: """create fractional extents from a given grid shape""" x_min = np.arange(0, 1, (1 / shape[1])) x_max = x_min + 1 / shape[1] From 528bb9d6ca51735fbca325a69970efa44b0d090f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 00:55:07 -0500 Subject: [PATCH 37/82] refactor, better organization --- fastplotlib/layouts/_engine.py | 143 ++++---- fastplotlib/layouts/_figure.py | 14 +- fastplotlib/layouts/_frame.py | 313 ++++++++++++++++ fastplotlib/layouts/_graphic_methods_mixin.py | 3 - fastplotlib/layouts/_imgui_figure.py | 2 +- fastplotlib/layouts/_plot_area.py | 3 +- fastplotlib/layouts/_rect.py | 31 +- fastplotlib/layouts/_subplot.py | 347 ++---------------- 8 files changed, 455 insertions(+), 401 deletions(-) create mode 100644 fastplotlib/layouts/_frame.py diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index ab2188d1d..0395fe44b 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -30,14 +30,39 @@ def __init__( renderer: pygfx.WgpuRenderer, subplots: np.ndarray[Subplot], canvas_rect: tuple, + moveable: bool, + resizeable: bool, ): self._renderer = renderer self._subplots: np.ndarray[Subplot] = subplots.ravel() self._canvas_rect = canvas_rect + self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( + [np.nan, np.nan] + ) + + self._active_action: str | None = None + self._active_subplot: Subplot | None = None + self._subplot_focus: Subplot | None = None + + for subplot in self._subplots: + # highlight plane when pointer enters it + subplot.frame.plane.add_event_handler( + partial(self._highlight_plane, subplot), "pointer_enter" + ) + + if resizeable: + # highlight/unhighlight resize handler when pointer enters/leaves + subplot.frame.resize_handle.add_event_handler( + partial(self._highlight_resize_handler, subplot), "pointer_enter" + ) + subplot.frame.resize_handle.add_event_handler( + partial(self._unhighlight_resize_handler, subplot), "pointer_leave" + ) + def _inside_render_rect(self, subplot: Subplot, pos: tuple[int, int]) -> bool: """whether the pos is within the render area, used for filtering out pointer events""" - rect = subplot._fpl_get_render_rect() + rect = subplot.frame.get_render_rect() x0, y0 = rect[:2] @@ -61,10 +86,33 @@ def set_rect(self, subplot, rect: np.ndarray | list | tuple): def set_extent(self, subplot, extent: np.ndarray | list | tuple): raise NotImplementedError - def _fpl_canvas_resized(self, canvas_rect: tuple): + def canvas_resized(self, canvas_rect: tuple): self._canvas_rect = canvas_rect for subplot in self._subplots: - subplot._fpl_canvas_resized(canvas_rect) + subplot.frame.canvas_resized(canvas_rect) + + def _highlight_resize_handler(self, subplot: Subplot, ev): + if self._active_action == "resize": + return + + ev.target.material.color = subplot.frame.resize_handle_color.highlight + + def _unhighlight_resize_handler(self, subplot: Subplot, ev): + if self._active_action == "resize": + return + + ev.target.material.color = subplot.frame.resize_handle_color.idle + + def _highlight_plane(self, subplot: Subplot, ev): + if self._active_action is not None: + return + + # reset color of previous focus + if self._subplot_focus is not None: + self._subplot_focus.frame.plane.material.color = subplot.frame.plane_color.idle + + self._subplot_focus = subplot + ev.target.material.color = subplot.frame.plane_color.highlight def __len__(self): return len(self._subplots) @@ -79,7 +127,7 @@ def __init__( moveable=True, resizeable=True, ): - super().__init__(renderer, subplots, canvas_rect) + super().__init__(renderer, subplots, canvas_rect, moveable, resizeable) self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( [np.nan, np.nan] @@ -90,30 +138,16 @@ def __init__( self._subplot_focus: Subplot | None = None for subplot in self._subplots: - # highlight plane when pointer enters it - subplot._fpl_plane.add_event_handler( - partial(self._highlight_plane, subplot), "pointer_enter" - ) - if moveable: # start a move action - subplot._fpl_plane.add_event_handler( + subplot.frame.plane.add_event_handler( partial(self._action_start, subplot, "move"), "pointer_down" ) # start a resize action - subplot._fpl_resize_handle.add_event_handler( + subplot.frame.resize_handle.add_event_handler( partial(self._action_start, subplot, "resize"), "pointer_down" ) - if resizeable: - # highlight/unhighlight resize handler when pointer enters/leaves - subplot._fpl_resize_handle.add_event_handler( - partial(self._highlight_resize_handler, subplot), "pointer_enter" - ) - subplot._fpl_resize_handle.add_event_handler( - partial(self._unhighlight_resize_handler, subplot), "pointer_leave" - ) - if moveable or resizeable: # when pointer moves, do an iteration of move or resize action self._renderer.add_event_handler(self._action_iter, "pointer_move") @@ -131,12 +165,12 @@ def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: delta_x, delta_y = delta if self._active_action == "resize": # subtract only from x1, y1 - new_extent = self._active_subplot.extent - np.asarray( + new_extent = self._active_subplot.frame.extent - np.asarray( [0, delta_x, 0, delta_y] ) else: # moving - new_extent = self._active_subplot.extent - np.asarray( + new_extent = self._active_subplot.frame.extent - np.asarray( [delta_x, delta_x, delta_y, delta_y] ) @@ -147,45 +181,45 @@ def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: # make sure width and height are valid # min width, height is 50px if w <= 50: # width > 0 - new_extent[:2] = self._active_subplot.extent[:2] + new_extent[:2] = self._active_subplot.frame.extent[:2] if h <= 50: # height > 0 - new_extent[2:] = self._active_subplot.extent[2:] + new_extent[2:] = self._active_subplot.frame.extent[2:] # ignore movement if this would cause an overlap - for frame in self._subplots: - if frame is self._active_subplot: + for subplot in self._subplots: + if subplot is self._active_subplot: continue - if frame.overlaps(new_extent): + if subplot.frame.rect_manager.overlaps(new_extent): # we have an overlap, need to ignore one or more deltas # ignore x - if not frame.is_left_of(x0) or not frame.is_right_of(x1): - new_extent[:2] = self._active_subplot.extent[:2] + if not subplot.frame.rect_manager.is_left_of(x0) or not subplot.frame.rect_manager.is_right_of(x1): + new_extent[:2] = self._active_subplot.frame.extent[:2] # ignore y - if not frame.is_above(y0) or not frame.is_below(y1): - new_extent[2:] = self._active_subplot.extent[2:] + if not subplot.frame.rect_manager.is_above(y0) or not subplot.frame.rect_manager.is_below(y1): + new_extent[2:] = self._active_subplot.frame.extent[2:] # make sure all vals are non-negative if (new_extent[:2] < 0).any(): # ignore delta_x - new_extent[:2] = self._active_subplot.extent[:2] + new_extent[:2] = self._active_subplot.frame.extent[:2] if (new_extent[2:] < 0).any(): # ignore delta_y - new_extent[2:] = self._active_subplot.extent[2:] + new_extent[2:] = self._active_subplot.frame.extent[2:] # canvas extent cx0, cy0, cw, ch = self._canvas_rect # check if new x-range is beyond canvas x-max if (new_extent[:2] > cx0 + cw).any(): - new_extent[:2] = self._active_subplot.extent[:2] + new_extent[:2] = self._active_subplot.frame.extent[:2] # check if new y-range is beyond canvas y-max if (new_extent[2:] > cy0 + ch).any(): - new_extent[2:] = self._active_subplot.extent[2:] + new_extent[2:] = self._active_subplot.frame.extent[2:] return new_extent @@ -196,11 +230,11 @@ def _action_start(self, subplot: Subplot, action: str, ev): if ev.button == 1: self._active_action = action if action == "resize": - subplot._fpl_resize_handle.material.color = ( - subplot.resize_handle_color.action + subplot.frame.resize_handle.material.color = ( + subplot.frame.resize_handle_color.action ) elif action == "move": - subplot._fpl_plane.material.color = subplot.plane_color.action + subplot.frame.plane.material.color = subplot.frame.plane_color.action else: raise ValueError @@ -213,45 +247,22 @@ def _action_iter(self, ev): delta_x, delta_y = self._last_pointer_pos - (ev.x, ev.y) new_extent = self._new_extent_from_delta((delta_x, delta_y)) - self._active_subplot.extent = new_extent + self._active_subplot.frame.extent = new_extent self._last_pointer_pos[:] = ev.x, ev.y def _action_end(self, ev): self._active_action = None if self._active_subplot is not None: - self._active_subplot._fpl_resize_handle.material.color = ( - self._active_subplot.resize_handle_color.idle + self._active_subplot.frame.resize_handle.material.color = ( + self._active_subplot.frame.resize_handle_color.idle ) - self._active_subplot._fpl_plane.material.color = ( - self._active_subplot.plane_color.idle + self._active_subplot.frame.plane.material.color = ( + self._active_subplot.frame.plane_color.idle ) self._active_subplot = None self._last_pointer_pos[:] = np.nan - def _highlight_resize_handler(self, subplot: Subplot, ev): - if self._active_action == "resize": - return - - ev.target.material.color = subplot.resize_handle_color.highlight - - def _unhighlight_resize_handler(self, subplot: Subplot, ev): - if self._active_action == "resize": - return - - ev.target.material.color = subplot.resize_handle_color.idle - - def _highlight_plane(self, subplot: Subplot, ev): - if self._active_action is not None: - return - - # reset color of previous focus - if self._subplot_focus is not None: - self._subplot_focus._fpl_plane.material.color = subplot.plane_color.idle - - self._subplot_focus = subplot - ev.target.material.color = subplot.plane_color.highlight - class GridLayout(FlexLayout): def __init__( diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 95b64b0da..dbfc2ba4c 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -151,7 +151,7 @@ def __init__( canvas, renderer, canvas_kwargs={"size": size} ) - canvas.add_event_handler(self._set_viewport_rects, "resize") + canvas.add_event_handler(self._reset_layout, "resize") if isinstance(cameras, str): # create the array representing the views for each subplot in the grid @@ -388,7 +388,7 @@ def __init__( self._underlay_scene = pygfx.Scene() for subplot in self._subplots.ravel(): - self._underlay_scene.add(subplot._world_object) + self._underlay_scene.add(subplot.frame._world_object) self._animate_funcs_pre: list[callable] = list() self._animate_funcs_post: list[callable] = list() @@ -551,7 +551,7 @@ def show( elif self.canvas.__class__.__name__ == "OffscreenRenderCanvas": # for test and docs gallery screenshots - self._set_viewport_rects() + self._reset_layout() for subplot in self: subplot.axes.update_using_camera() @@ -722,9 +722,9 @@ def export(self, uri: str | Path | bytes, **kwargs): def open_popup(self, *args, **kwargs): warn("popups only supported by ImguiFigure") - def _set_viewport_rects(self, *ev): + def _reset_layout(self, *ev): """set the viewport rects for all subplots, *ev argument is not used, exists because of renderer resize event""" - self.layout._fpl_canvas_resized(self.get_pygfx_render_area()) + self.layout.canvas_resized(self.get_pygfx_render_area()) def get_pygfx_render_area(self, *args) -> tuple[float, float, float, float]: """ @@ -782,8 +782,8 @@ def remove_subplot(self, subplot: Subplot): raise KeyError(f"given subplot: {subplot} not found in the layout.") subplot.clear() - self._underlay_scene.remove(subplot._world_object) - subplot._world_object.clear() + self._underlay_scene.remove(subplot.frame._world_object) + subplot.frame._world_object.clear() self.layout._subplots = None subplots = self._subplots.tolist() subplots.remove(subplot) diff --git a/fastplotlib/layouts/_frame.py b/fastplotlib/layouts/_frame.py new file mode 100644 index 000000000..83829e15d --- /dev/null +++ b/fastplotlib/layouts/_frame.py @@ -0,0 +1,313 @@ +import numpy as np +import pygfx + +from ._rect import RectManager +from ._utils import IMGUI, IMGUI_TOOLBAR_HEIGHT +from ..utils._types import SelectorColorStates +from ..graphics import TextGraphic + + +""" +Each Subplot is framed by a 2D plane mesh, a rectangle. +The rectangles are viewed using the UnderlayCamera where (0, 0) is the top left corner. +We can control the bbox of this rectangle by changing the x and y boundaries of the rectangle. + +Note how the y values of the plane mesh are negative, this is because of the UnderlayCamera. +We always just keep the positive y value, and make it negative only when setting the plane mesh. + +Illustration: + +(0, 0) --------------------------------------------------- +---------------------------------------------------------- +---------------------------------------------------------- +--------------(x0, -y0) --------------- (x1, -y0) -------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||rectangle|||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +------------------------|||||||||||||||------------------- +--------------(x0, -y1) --------------- (x1, -y1)--------- +---------------------------------------------------------- +------------------------------------------- (canvas_width, canvas_height) + +""" + + +# wgsl shader snippet for SDF function that defines the resize handler, a lower right triangle. +sdf_wgsl_resize_handler = """ +// hardcode square root of 2 +let m_sqrt_2 = 1.4142135; + +// given a distance from an origin point, this defines the hypotenuse of a lower right triangle +let distance = (-coord.x + coord.y) / m_sqrt_2; + +// return distance for this position +return distance * size; +""" + + +class MeshMasks: + """Used set the x1, x1, y0, y1 positions of the mesh""" + + x0 = np.array( + [ + [False, False, False], + [True, False, False], + [False, False, False], + [True, False, False], + ] + ) + + x1 = np.array( + [ + [True, False, False], + [False, False, False], + [True, False, False], + [False, False, False], + ] + ) + + y0 = np.array( + [ + [False, True, False], + [False, True, False], + [False, False, False], + [False, False, False], + ] + ) + + y1 = np.array( + [ + [False, False, False], + [False, False, False], + [False, True, False], + [False, True, False], + ] + ) + + +masks = MeshMasks + + +class Frame: + resize_handle_color = SelectorColorStates( + idle=(0.6, 0.6, 0.6, 1), # gray + highlight=(1, 1, 1, 1), # white + action=(1, 0, 1, 1), # magenta + ) + + plane_color = SelectorColorStates( + idle=(0.1, 0.1, 0.1), # dark grey + highlight=(0.2, 0.2, 0.2), # less dark grey + action=(0.1, 0.1, 0.2), # dark gray-blue + ) + + def __init__(self, viewport, rect, extent, resizeable, title, docks, toolbar_visible, canvas_rect): + self.viewport = viewport + self.docks = docks + self._toolbar_visible = toolbar_visible + + if rect is not None: + self._rect_manager = RectManager(*rect, canvas_rect) + elif extent is not None: + self._rect_manager = RectManager.from_extent( + extent, canvas_rect + ) + else: + raise ValueError("Must provide `rect` or `extent`") + + wobjects = list() + + if title is None: + title_text = "" + else: + title_text = title + self._title_graphic = TextGraphic(title_text, font_size=16, face_color="white") + wobjects.append(self._title_graphic.world_object) + + # init mesh of size 1 to graphically represent rect + geometry = pygfx.plane_geometry(1, 1) + material = pygfx.MeshBasicMaterial(color=self.plane_color.idle, pick_write=True) + self._plane = pygfx.Mesh(geometry, material) + wobjects.append(self._plane) + + # otherwise text isn't visible + self._plane.world.z = 0.5 + + # create resize handler at point (x1, y1) + x1, y1 = self.extent[[1, 3]] + self._resize_handle = pygfx.Points( + # note negative y since y is inverted in UnderlayCamera + # subtract 7 so that the bottom right corner of the triangle is at the center + pygfx.Geometry(positions=[[x1 - 7, -y1 + 7, 0]]), + pygfx.PointsMarkerMaterial( + color=self.resize_handle_color.idle, + marker="custom", + custom_sdf=sdf_wgsl_resize_handler, + size=12, + size_space="screen", + pick_write=True, + ), + ) + + if not resizeable: + c = (0, 0, 0, 0) + self._resize_handle.material.color = c + self._resize_handle.material.edge_width = 0 + self.resize_handle_color = SelectorColorStates(c, c, c) + + wobjects.append(self._resize_handle) + + self._reset_plane() + self.reset_viewport() + + self._world_object = pygfx.Group() + self._world_object.add(*wobjects) + + @property + def rect_manager(self) -> RectManager: + return self._rect_manager + + @property + def extent(self) -> np.ndarray: + """extent, (xmin, xmax, ymin, ymax)""" + # not actually stored, computed when needed + return self._rect_manager.extent + + @extent.setter + def extent(self, extent): + self._rect_manager.extent = extent + self._reset_plane() + self.reset_viewport() + + @property + def rect(self) -> np.ndarray[int]: + """rect in absolute screen space, (x, y, w, h)""" + return self._rect_manager.rect + + @rect.setter + def rect(self, rect: np.ndarray): + self._rect_manager.rect = rect + self._reset_plane() + self.reset_viewport() + + def reset_viewport(self): + # get rect of the render area + x, y, w, h = self.get_render_rect() + + s_left = self.docks["left"].size + s_top = self.docks["top"].size + s_right = self.docks["right"].size + s_bottom = self.docks["bottom"].size + + # top and bottom have same width + # subtract left and right dock sizes + w_top_bottom = w - s_left - s_right + # top and bottom have same x pos + x_top_bottom = x + s_left + + # set dock rects + self.docks["left"].viewport.rect = x, y, s_left, h + self.docks["top"].viewport.rect = x_top_bottom, y, w_top_bottom, s_top + self.docks["bottom"].viewport.rect = ( + x_top_bottom, + y + h - s_bottom, + w_top_bottom, + s_bottom, + ) + self.docks["right"].viewport.rect = x + w - s_right, y, s_right, h + + # calc subplot rect by adjusting for dock sizes + x += s_left + y += s_top + w -= s_left + s_right + h -= s_top + s_bottom + + # set subplot rect + self.viewport.rect = x, y, w, h + + def get_render_rect(self) -> tuple[float, float, float, float]: + """ + Get the actual render area of the subplot, including the docks. + + Excludes area taken by the subplot title and toolbar. Also adds a small amount of spacing around the subplot. + """ + x, y, w, h = self.rect + + x += 1 # add 1 so a 1 pixel edge is visible + w -= 2 # subtract 2, so we get a 1 pixel edge on both sides + + y = ( + y + 4 + self._title_graphic.font_size + 4 + ) # add 4 pixels above and below title for better spacing + + if self.toolbar_visible: + toolbar_space = IMGUI_TOOLBAR_HEIGHT + resize_handle_space = 0 + else: + toolbar_space = 0 + # need some space for resize handler if imgui toolbar isn't present + resize_handle_space = 13 + + # adjust for the 4 pixels from the line above + # also give space for resize handler if imgui toolbar is not present + h = h - 4 - self._title_graphic.font_size - toolbar_space - 4 - resize_handle_space + + return x, y, w, h + + def _reset_plane(self): + """reset the plane mesh using the current rect state""" + + x0, x1, y0, y1 = self._rect_manager.extent + w = self._rect_manager.w + + self._plane.geometry.positions.data[masks.x0] = x0 + self._plane.geometry.positions.data[masks.x1] = x1 + self._plane.geometry.positions.data[masks.y0] = ( + -y0 + ) # negative y because UnderlayCamera y is inverted + self._plane.geometry.positions.data[masks.y1] = -y1 + + self._plane.geometry.positions.update_full() + + # note negative y since y is inverted in UnderlayCamera + # subtract 7 so that the bottom right corner of the triangle is at the center + self._resize_handle.geometry.positions.data[0] = [x1 - 7, -y1 + 7, 0] + self._resize_handle.geometry.positions.update_full() + + # set subplot title position + x = x0 + (w / 2) + y = y0 + (self._title_graphic.font_size / 2) + self._title_graphic.world_object.world.x = x + self._title_graphic.world_object.world.y = -y - 4 # add 4 pixels for spacing + + @property + def toolbar_visible(self) -> bool: + return self._toolbar_visible + + @toolbar_visible.setter + def toolbar_visible(self, visible: bool): + self._toolbar_visible = visible + self.reset_viewport() + + @property + def title_graphic(self) -> TextGraphic: + return self._title_graphic + + @property + def plane(self) -> pygfx.Mesh: + """the plane mesh""" + return self._plane + + @property + def resize_handle(self) -> pygfx.Points: + """resize handler point""" + return self._resize_handle + + def canvas_resized(self, canvas_rect): + """called by layout is resized""" + self._rect_manager.canvas_resized(canvas_rect) + self._reset_plane() + self.reset_viewport() diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index a08e9b110..a04b681f5 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -9,9 +9,6 @@ class GraphicMethodsMixin: - def __init__(self): - pass - def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic: if "center" in kwargs.keys(): center = kwargs.pop("center") diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index 14cf77456..3a73afc58 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -168,7 +168,7 @@ def add_gui(self, gui: EdgeWindow): self.guis[location] = gui - self._set_viewport_rects() + self._reset_layout() def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: """ diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index f60f5149d..2e69af100 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -11,6 +11,7 @@ from ._utils import create_controller from ..graphics._base import Graphic from ..graphics.selectors._base_selector import BaseSelector +from ._graphic_methods_mixin import GraphicMethodsMixin from ..legends import Legend @@ -24,7 +25,7 @@ IPYTHON = get_ipython() -class PlotArea: +class PlotArea(GraphicMethodsMixin): def __init__( self, parent: Union["PlotArea", "Figure"], diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index 481d69d8b..17c9feb82 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -89,7 +89,7 @@ def rect(self) -> np.ndarray: def rect(self, rect: np.ndarray | tuple): self._set(rect) - def _fpl_canvas_resized(self, canvas_rect: tuple): + def canvas_resized(self, canvas_rect: tuple): # called by layout when canvas is resized self._canvas_rect[:] = canvas_rect # set new rect using existing rect_frac since this remains constant regardless of resize @@ -190,6 +190,35 @@ def validate_extent(extent: np.ndarray | tuple, canvas_rect: tuple): f"extent: {extent} y-range is beyond the bounds of the canvas: {canvas_extent}" ) + def is_above(self, y0, dist: int = 1) -> bool: + # our bottom < other top within given distance + return self.y1 < y0 + dist + + def is_below(self, y1, dist: int = 1) -> bool: + # our top > other bottom + return self.y0 > y1 - dist + + def is_left_of(self, x0, dist: int = 1) -> bool: + # our right_edge < other left_edge + # self.x1 < other.x0 + return self.x1 < x0 + dist + + def is_right_of(self, x1, dist: int = 1) -> bool: + # self.x0 > other.x1 + return self.x0 > x1 - dist + + def overlaps(self, extent: np.ndarray) -> bool: + """returns whether this subplot overlaps with the given extent""" + x0, x1, y0, y1 = extent + return not any( + [ + self.is_above(y0), + self.is_below(y1), + self.is_left_of(x0), + self.is_right_of(x1), + ] + ) + def __repr__(self): s = f"{self._rect_frac}\n{self.rect}" diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 73ff0608a..a9a801cac 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -5,111 +5,14 @@ import pygfx from rendercanvas import BaseRenderCanvas -from ._rect import RectManager from ..graphics import TextGraphic -from ._utils import create_camera, create_controller, IMGUI, IMGUI_TOOLBAR_HEIGHT +from ._utils import create_camera, create_controller, IMGUI from ._plot_area import PlotArea -from ._graphic_methods_mixin import GraphicMethodsMixin +from ._frame import Frame from ..graphics._axes import Axes -from ..utils._types import SelectorColorStates - - -""" -Each subplot is defined by a 2D plane mesh, a rectangle. -The rectangles are viewed using the UnderlayCamera where (0, 0) is the top left corner. -We can control the bbox of this rectangle by changing the x and y boundaries of the rectangle. - -Note how the y values of the plane mesh are negative, this is because of the UnderlayCamera. -We always just keep the positive y value, and make it negative only when setting the plane mesh. - -Illustration: - -(0, 0) --------------------------------------------------- ----------------------------------------------------------- ----------------------------------------------------------- ---------------(x0, -y0) --------------- (x1, -y0) -------- -------------------------|||||||||||||||------------------- -------------------------|||||||||||||||------------------- -------------------------|||||||||||||||------------------- -------------------------|||rectangle|||------------------- -------------------------|||||||||||||||------------------- -------------------------|||||||||||||||------------------- -------------------------|||||||||||||||------------------- ---------------(x0, -y1) --------------- (x1, -y1)--------- ----------------------------------------------------------- -------------------------------------------- (canvas_width, canvas_height) - -""" - -# wgsl shader snippet for SDF function that defines the resize handler, a lower right triangle. -sdf_wgsl_resize_handler = """ -// hardcode square root of 2 -let m_sqrt_2 = 1.4142135; - -// given a distance from an origin point, this defines the hypotenuse of a lower right triangle -let distance = (-coord.x + coord.y) / m_sqrt_2; - -// return distance for this position -return distance * size; -""" - - -class MeshMasks: - """Used set the x1, x1, y0, y1 positions of the mesh""" - - x0 = np.array( - [ - [False, False, False], - [True, False, False], - [False, False, False], - [True, False, False], - ] - ) - - x1 = np.array( - [ - [True, False, False], - [False, False, False], - [True, False, False], - [False, False, False], - ] - ) - - y0 = np.array( - [ - [False, True, False], - [False, True, False], - [False, False, False], - [False, False, False], - ] - ) - - y1 = np.array( - [ - [False, False, False], - [False, False, False], - [False, True, False], - [False, True, False], - ] - ) - - -masks = MeshMasks - - -class Subplot(PlotArea, GraphicMethodsMixin): - resize_handle_color = SelectorColorStates( - idle=(0.6, 0.6, 0.6, 1), # gray - highlight=(1, 1, 1, 1), # white - action=(1, 0, 1, 1), # magenta - ) - - plane_color = SelectorColorStates( - idle=(0.1, 0.1, 0.1), # dark grey - highlight=(0.2, 0.2, 0.2), # less dark grey - action=(0.1, 0.1, 0.2), # dark gray-blue - ) + +class Subplot(PlotArea): def __init__( self, parent: Union["Figure"], @@ -157,8 +60,6 @@ def __init__( """ - super(GraphicMethodsMixin, self).__init__() - camera = create_camera(camera) controller = create_controller(controller_type=controller, camera=camera) @@ -166,11 +67,11 @@ def __init__( self._docks = dict() if IMGUI: - self._toolbar = True + toolbar_visible = True else: - self._toolbar = False + toolbar_visible = False - super(Subplot, self).__init__( + super().__init__( parent=parent, camera=camera, controller=controller, @@ -189,61 +90,16 @@ def __init__( self._axes = Axes(self) self.scene.add(self.axes.world_object) - if rect is not None: - self._rect = RectManager(*rect, self.get_figure().get_pygfx_render_area()) - elif extent is not None: - self._rect = RectManager.from_extent( - extent, self.get_figure().get_pygfx_render_area() - ) - else: - raise ValueError("Must provide `rect` or `extent`") - - wobjects = list() - - if name is None: - title_text = "" - else: - title_text = name - self._title_graphic = TextGraphic(title_text, font_size=16, face_color="white") - wobjects.append(self._title_graphic.world_object) - - # init mesh of size 1 to graphically represent rect - geometry = pygfx.plane_geometry(1, 1) - material = pygfx.MeshBasicMaterial(color=self.plane_color.idle, pick_write=True) - self._plane = pygfx.Mesh(geometry, material) - wobjects.append(self._plane) - - # otherwise text isn't visible - self._plane.world.z = 0.5 - - # create resize handler at point (x1, y1) - x1, y1 = self.extent[[1, 3]] - self._resize_handle = pygfx.Points( - # note negative y since y is inverted in UnderlayCamera - # subtract 7 so that the bottom right corner of the triangle is at the center - pygfx.Geometry(positions=[[x1 - 7, -y1 + 7, 0]]), - pygfx.PointsMarkerMaterial( - color=self.resize_handle_color.idle, - marker="custom", - custom_sdf=sdf_wgsl_resize_handler, - size=12, - size_space="screen", - pick_write=True, - ), + self._frame = Frame( + viewport=self.viewport, + rect=rect, + extent=extent, + resizeable=resizeable, + title=name, + docks=self.docks, + toolbar_visible=toolbar_visible, + canvas_rect=parent.get_pygfx_render_area() ) - if not resizeable: - c = (0, 0, 0, 0) - self._resize_handle.material.color = c - self._resize_handle.material.edge_width = 0 - self.resize_handle_color = SelectorColorStates(c, c, c) - - wobjects.append(self._resize_handle) - - self._reset_plane() - self._reset_viewport_rect() - - self._world_object = pygfx.Group() - self._world_object.add(*wobjects) @property def axes(self) -> Axes: @@ -285,12 +141,12 @@ def docks(self) -> dict: @property def toolbar(self) -> bool: """show/hide toolbar""" - return self._toolbar + return self.frame.toolbar_visible @toolbar.setter def toolbar(self, visible: bool): - self._toolbar = bool(visible) - self.get_figure()._set_viewport_rects(self) + self.frame.toolbar_visible = visible + self.frame.reset_viewport() def _render(self): self.axes.update_using_camera() @@ -299,170 +155,17 @@ def _render(self): @property def title(self) -> TextGraphic: """subplot title""" - return self._title_graphic + return self._frame.title_graphic @title.setter def title(self, text: str): text = str(text) - self._title_graphic.text = text - - @property - def extent(self) -> np.ndarray: - """extent, (xmin, xmax, ymin, ymax)""" - # not actually stored, computed when needed - return self._rect.extent - - @extent.setter - def extent(self, extent): - self._rect.extent = extent - self._reset_plane() - self._reset_viewport_rect() + self.title.text = text @property - def rect(self) -> np.ndarray[int]: - """rect in absolute screen space, (x, y, w, h)""" - return self._rect.rect - - @rect.setter - def rect(self, rect: np.ndarray): - self._rect.rect = rect - self._reset_plane() - self._reset_viewport_rect() - - def _reset_viewport_rect(self): - # get rect of the render area - x, y, w, h = self._fpl_get_render_rect() - - s_left = self.docks["left"].size - s_top = self.docks["top"].size - s_right = self.docks["right"].size - s_bottom = self.docks["bottom"].size - - # top and bottom have same width - # subtract left and right dock sizes - w_top_bottom = w - s_left - s_right - # top and bottom have same x pos - x_top_bottom = x + s_left - - # set dock rects - self.docks["left"].viewport.rect = x, y, s_left, h - self.docks["top"].viewport.rect = x_top_bottom, y, w_top_bottom, s_top - self.docks["bottom"].viewport.rect = ( - x_top_bottom, - y + h - s_bottom, - w_top_bottom, - s_bottom, - ) - self.docks["right"].viewport.rect = x + w - s_right, y, s_right, h - - # calc subplot rect by adjusting for dock sizes - x += s_left - y += s_top - w -= s_left + s_right - h -= s_top + s_bottom - - # set subplot rect - self.viewport.rect = x, y, w, h - - def _fpl_get_render_rect(self) -> tuple[float, float, float, float]: - """ - Get the actual render area of the subplot, including the docks. - - Excludes area taken by the subplot title and toolbar. Also adds a small amount of spacing around the subplot. - """ - x, y, w, h = self.rect - - x += 1 # add 1 so a 1 pixel edge is visible - w -= 2 # subtract 2, so we get a 1 pixel edge on both sides - - y = ( - y + 4 + self.title.font_size + 4 - ) # add 4 pixels above and below title for better spacing - - if self.toolbar: - toolbar_space = IMGUI_TOOLBAR_HEIGHT - resize_handle_space = 0 - else: - toolbar_space = 0 - # need some space for resize handler if imgui toolbar isn't present - resize_handle_space = 13 - - # adjust for the 4 pixels from the line above - # also give space for resize handler if imgui toolbar is not present - h = h - 4 - self.title.font_size - toolbar_space - 4 - resize_handle_space - - return x, y, w, h - - def _reset_plane(self): - """reset the plane mesh using the current rect state""" - - x0, x1, y0, y1 = self._rect.extent - w = self._rect.w - - self._plane.geometry.positions.data[masks.x0] = x0 - self._plane.geometry.positions.data[masks.x1] = x1 - self._plane.geometry.positions.data[masks.y0] = ( - -y0 - ) # negative y because UnderlayCamera y is inverted - self._plane.geometry.positions.data[masks.y1] = -y1 - - self._plane.geometry.positions.update_full() - - # note negative y since y is inverted in UnderlayCamera - # subtract 7 so that the bottom right corner of the triangle is at the center - self._resize_handle.geometry.positions.data[0] = [x1 - 7, -y1 + 7, 0] - self._resize_handle.geometry.positions.update_full() - - # set subplot title position - x = x0 + (w / 2) - y = y0 + (self.title.font_size / 2) - self.title.world_object.world.x = x - self.title.world_object.world.y = -y - 4 # add 4 pixels for spacing - - @property - def _fpl_plane(self) -> pygfx.Mesh: - """the plane mesh""" - return self._plane - - @property - def _fpl_resize_handle(self) -> pygfx.Points: - """resize handler point""" - return self._resize_handle - - def _fpl_canvas_resized(self, canvas_rect): - """called by layout is resized""" - self._rect._fpl_canvas_resized(canvas_rect) - self._reset_plane() - self._reset_viewport_rect() - - def is_above(self, y0, dist: int = 1) -> bool: - # our bottom < other top within given distance - return self._rect.y1 < y0 + dist - - def is_below(self, y1, dist: int = 1) -> bool: - # our top > other bottom - return self._rect.y0 > y1 - dist - - def is_left_of(self, x0, dist: int = 1) -> bool: - # our right_edge < other left_edge - # self.x1 < other.x0 - return self._rect.x1 < x0 + dist - - def is_right_of(self, x1, dist: int = 1) -> bool: - # self.x0 > other.x1 - return self._rect.x0 > x1 - dist - - def overlaps(self, extent: np.ndarray) -> bool: - """returns whether this subplot overlaps with the given extent""" - x0, x1, y0, y1 = extent - return not any( - [ - self.is_above(y0), - self.is_below(y1), - self.is_left_of(x0), - self.is_right_of(x1), - ] - ) + def frame(self) -> Frame: + """Frame that the subplot lives in""" + return self._frame class Dock(PlotArea): @@ -503,7 +206,7 @@ def size(self) -> int: @size.setter def size(self, s: int): self._size = s - self.parent._reset_viewport_rect() + self.parent.reset_viewport() def _render(self): if self.size == 0: From 4a20f341cd44b3cb0700c9bf94168241672292cf Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 01:18:01 -0500 Subject: [PATCH 38/82] modified: scripts/generate_add_graphic_methods.py --- scripts/generate_add_graphic_methods.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/generate_add_graphic_methods.py b/scripts/generate_add_graphic_methods.py index d69185521..533ae77c6 100644 --- a/scripts/generate_add_graphic_methods.py +++ b/scripts/generate_add_graphic_methods.py @@ -34,8 +34,6 @@ def generate_add_graphics_methods(): f.write("from ..graphics._base import Graphic\n\n") f.write("\nclass GraphicMethodsMixin:\n") - f.write(" def __init__(self):\n") - f.write(" pass\n\n") f.write( " def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic:\n" From 3c39b3a14e873d7e3d3598ea1b0508ab871dd0de Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 01:22:52 -0500 Subject: [PATCH 39/82] more stuff --- fastplotlib/layouts/_engine.py | 85 ++++++++++++++++++++++------------ fastplotlib/layouts/_figure.py | 4 ++ 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 0395fe44b..baaa35b42 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -4,6 +4,7 @@ import pygfx from ._subplot import Subplot +from ._rect import RectManager class UnderlayCamera(pygfx.Camera): @@ -29,7 +30,7 @@ def __init__( self, renderer: pygfx.WgpuRenderer, subplots: np.ndarray[Subplot], - canvas_rect: tuple, + canvas_rect: tuple[float, float], moveable: bool, resizeable: bool, ): @@ -74,18 +75,6 @@ def _inside_render_rect(self, subplot: Subplot, pos: tuple[int, int]) -> bool: return False - def add_subplot(self): - raise NotImplementedError - - def remove_subplot(self, subplot): - raise NotImplementedError - - def set_rect(self, subplot, rect: np.ndarray | list | tuple): - raise NotImplementedError - - def set_extent(self, subplot, extent: np.ndarray | list | tuple): - raise NotImplementedError - def canvas_resized(self, canvas_rect: tuple): self._canvas_rect = canvas_rect for subplot in self._subplots: @@ -155,12 +144,6 @@ def __init__( # end the action when pointer button goes up self._renderer.add_event_handler(self._action_end, "pointer_up") - def remove_subplot(self, subplot): - if subplot is self._active_subplot: - self._active_subplot = None - if subplot is self._subplot_focus: - self._subplot_focus = None - def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: delta_x, delta_y = delta if self._active_action == "resize": @@ -263,6 +246,54 @@ def _action_end(self, ev): self._last_pointer_pos[:] = np.nan + def set_rect(self, subplot: Subplot, rect: tuple | list | np.ndarray): + """ + Set the rect of a Subplot + + Parameters + ---------- + subplot: Subplot + the subplot to set the rect of + + rect: (x, y, w, h) + as absolute pixels or fractional + + """ + + new_rect = RectManager(*rect, self._canvas_rect) + extent = new_rect.extent + # check for overlaps + for s in self._subplots: + if s is subplot: + continue + + if s.frame.rect_manager.overlaps(extent): + raise ValueError(f"Given rect: {rect} overlaps with another subplot.") + + def set_extent(self, subplot: Subplot, extent: tuple | list | np.ndarray): + """ + Set the extent of a Subplot + + Parameters + ---------- + subplot: Subplot + the subplot to set the extent of + + extent: (xmin, xmax, ymin, ymax) + as absolute pixels or fractional + + """ + + new_rect = RectManager.from_extent(extent, self._canvas_rect) + extent = new_rect.extent + # check for overlaps + for s in self._subplots: + if s is subplot: + continue + + if s.frame.rect_manager.overlaps(extent): + raise ValueError(f"Given extent: {extent} overlaps with another subplot.") + class GridLayout(FlexLayout): def __init__( @@ -295,23 +326,19 @@ def set_extent(self, subplot, extent: np.ndarray | list | tuple): ) def add_row(self): - pass - # new_shape = (self.shape[0] + 1, self.shape[1]) - # extents = get_extents_from_grid(new_shape) - # for subplot, extent in zip(self._subplots, extents): - # subplot.extent = extent + raise NotImplementedError("Not yet implemented") def add_column(self): - pass + raise NotImplementedError("Not yet implemented") def remove_row(self): - pass + raise NotImplementedError("Not yet implemented") def remove_column(self): - pass + raise NotImplementedError("Not yet implemented") def add_subplot(self): - raise NotImplementedError + raise NotImplementedError("Not implemented for GridLayout which is an auto layout manager") def remove_subplot(self, subplot): - raise NotImplementedError + raise NotImplementedError("Not implemented for GridLayout which is an auto layout manager") diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index dbfc2ba4c..350807ad9 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -755,6 +755,8 @@ def add_subplot( "`add_subplot()` is not implemented for Figures using a GridLayout" ) + raise NotImplementedError("Not yet implemented") + camera = create_camera(camera) controller = create_controller(controller, camera) @@ -773,6 +775,8 @@ def add_subplot( return subplot def remove_subplot(self, subplot: Subplot): + raise NotImplementedError("Not yet implemented") + if isinstance(self.layout, GridLayout): raise NotImplementedError( "`remove_subplot()` is not implemented for Figures using a GridLayout" From be56f343cf0d12b16e02505298ab2e78a9a8d2b8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 03:45:16 -0400 Subject: [PATCH 40/82] more --- fastplotlib/ui/_subplot_toolbar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/ui/_subplot_toolbar.py b/fastplotlib/ui/_subplot_toolbar.py index aa599838c..a06e81b90 100644 --- a/fastplotlib/ui/_subplot_toolbar.py +++ b/fastplotlib/ui/_subplot_toolbar.py @@ -17,7 +17,7 @@ def __init__(self, subplot: Subplot, fa_icons: imgui.ImFont): def update(self): # get subplot rect - x, y, width, height = self._subplot.rect + x, y, width, height = self._subplot.frame.rect # place the toolbar window below the subplot pos = (x + 1, y + height - IMGUI_TOOLBAR_HEIGHT) From 79430000669d92e31776e86eda3e2df4289ff806 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 03:46:09 -0400 Subject: [PATCH 41/82] add examples --- examples/flex_layouts/extent_frac_layout.py | 71 +++++++++++++++++++++ examples/flex_layouts/extent_layout.py | 71 +++++++++++++++++++++ examples/flex_layouts/rect_frac_layout.py | 71 +++++++++++++++++++++ examples/flex_layouts/rect_layout.py | 71 +++++++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 examples/flex_layouts/extent_frac_layout.py create mode 100644 examples/flex_layouts/extent_layout.py create mode 100644 examples/flex_layouts/rect_frac_layout.py create mode 100644 examples/flex_layouts/rect_layout.py diff --git a/examples/flex_layouts/extent_frac_layout.py b/examples/flex_layouts/extent_frac_layout.py new file mode 100644 index 000000000..8f9709765 --- /dev/null +++ b/examples/flex_layouts/extent_frac_layout.py @@ -0,0 +1,71 @@ +""" +Fractional Extent Layout +======================== + +Create subplots using extents given as fractions of the canvas. This example plots two images and their histograms in +separate subplots + +""" + +import numpy as np +import imageio.v3 as iio +import fastplotlib as fpl + +# load images +img1 = iio.imread("imageio:astronaut.png") +img2 = iio.imread("imageio:wikkie.png") + +# calculate histograms +hist_1, edges_1 = np.histogram(img1) +centers_1 = edges_1[:-1] + np.diff(edges_1) / 2 + +hist_2, edges_2 = np.histogram(img2) +centers_2 = edges_2[:-1] + np.diff(edges_2) / 2 + +# figure size in pixels +size = (700, 560) + +# extent is (xmin, xmax, ymin, ymax) +# here it is defined as fractions of the canvas +extents = [ + (0, 0.3, 0, 0.5), # for image1 + (0, 0.3, 0.5, 1), # for image2 + (0.3, 1, 0, 0.5), # for image1 histogram + (0.3, 1, 0.5, 1), # for image2 histogram +] + +# create a figure using the rects and size +# also give each subplot a name +figure = fpl.Figure( + extents=extents, + names=["astronaut image", "wikkie image", "astronaut histogram", "wikkie histogram"], + size=size +) + +# add image to the corresponding subplots +figure["astronaut image"].add_image(img1) +figure["wikkie image"].add_image(img2) + +# add histogram to the corresponding subplots +figure["astronaut histogram"].add_line(np.column_stack([centers_1, hist_1])) +figure["wikkie histogram"].add_line(np.column_stack([centers_2, hist_2])) + + +for subplot in figure: + if "image" in subplot.name: + # remove axes from image subplots to reduce clutter + subplot.axes.visible = False + continue + + # don't maintain aspect ratio for the histogram subplots + subplot.camera.maintain_aspect = False + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/flex_layouts/extent_layout.py b/examples/flex_layouts/extent_layout.py new file mode 100644 index 000000000..d1badd497 --- /dev/null +++ b/examples/flex_layouts/extent_layout.py @@ -0,0 +1,71 @@ +""" +Extent Layout +============= + +Create subplots using given extents in absolute pixels. This example plots two images and their histograms in +separate subplots + +""" + +import numpy as np +import imageio.v3 as iio +import fastplotlib as fpl + +# load images +img1 = iio.imread("imageio:astronaut.png") +img2 = iio.imread("imageio:wikkie.png") + +# calculate histograms +hist_1, edges_1 = np.histogram(img1) +centers_1 = edges_1[:-1] + np.diff(edges_1) / 2 + +hist_2, edges_2 = np.histogram(img2) +centers_2 = edges_2[:-1] + np.diff(edges_2) / 2 + +# figure size in pixels +size = (700, 560) + +# extent is (xmin, xmax, ymin, ymax) +# here it is defined in absolute pixels +extents = [ + (0, 200, 0, 280), # for image1 + (0, 200, 280, 560), # for image2 + (200, 700, 0, 280), # for image1 histogram + (200, 700, 280, 560), # for image2 histogram +] + +# create a figure using the rects and size +# also give each subplot a name +figure = fpl.Figure( + extents=extents, + names=["astronaut image", "wikkie image", "astronaut histogram", "wikkie histogram"], + size=size +) + +# add image to the corresponding subplots +figure["astronaut image"].add_image(img1) +figure["wikkie image"].add_image(img2) + +# add histogram to the corresponding subplots +figure["astronaut histogram"].add_line(np.column_stack([centers_1, hist_1])) +figure["wikkie histogram"].add_line(np.column_stack([centers_2, hist_2])) + + +for subplot in figure: + if "image" in subplot.name: + # remove axes from image subplots to reduce clutter + subplot.axes.visible = False + continue + + # don't maintain aspect ratio for the histogram subplots + subplot.camera.maintain_aspect = False + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/flex_layouts/rect_frac_layout.py b/examples/flex_layouts/rect_frac_layout.py new file mode 100644 index 000000000..96d0fd063 --- /dev/null +++ b/examples/flex_layouts/rect_frac_layout.py @@ -0,0 +1,71 @@ +""" +Rect Fractional Layout +====================== + +Create subplots using rects given as fractions of the canvas. This example plots two images and their histograms in +separate subplots + +""" + +import numpy as np +import imageio.v3 as iio +import fastplotlib as fpl + +# load images +img1 = iio.imread("imageio:astronaut.png") +img2 = iio.imread("imageio:wikkie.png") + +# calculate histograms +hist_1, edges_1 = np.histogram(img1) +centers_1 = edges_1[:-1] + np.diff(edges_1) / 2 + +hist_2, edges_2 = np.histogram(img2) +centers_2 = edges_2[:-1] + np.diff(edges_2) / 2 + +# figure size in pixels +size = (700, 560) + +# rect is (x, y, width, height) +# here it is defined as fractions of the canvas +rects = [ + (0, 0, 0.3, 0.5), # for image1 + (0, 0.5, 0.3, 0.5), # for image2 + (0.3, 0, 0.7, 0.5), # for image1 histogram + (0.3, 0.5, 0.7, 0.5), # for image2 histogram +] + +# create a figure using the rects and size +# also give each subplot a name +figure = fpl.Figure( + rects=rects, + names=["astronaut image", "wikkie image", "astronaut histogram", "wikkie histogram"], + size=size +) + +# add image to the corresponding subplots +figure["astronaut image"].add_image(img1) +figure["wikkie image"].add_image(img2) + +# add histogram to the corresponding subplots +figure["astronaut histogram"].add_line(np.column_stack([centers_1, hist_1])) +figure["wikkie histogram"].add_line(np.column_stack([centers_2, hist_2])) + + +for subplot in figure: + if "image" in subplot.name: + # remove axes from image subplots to reduce clutter + subplot.axes.visible = False + continue + + # don't maintain aspect ratio for the histogram subplots + subplot.camera.maintain_aspect = False + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/flex_layouts/rect_layout.py b/examples/flex_layouts/rect_layout.py new file mode 100644 index 000000000..96bce3cca --- /dev/null +++ b/examples/flex_layouts/rect_layout.py @@ -0,0 +1,71 @@ +""" +Rect Layout +=========== + +Create subplots using given rects in absolute pixels. This example plots two images and their histograms in +separate subplots + +""" + +import numpy as np +import imageio.v3 as iio +import fastplotlib as fpl + +# load images +img1 = iio.imread("imageio:astronaut.png") +img2 = iio.imread("imageio:wikkie.png") + +# calculate histograms +hist_1, edges_1 = np.histogram(img1) +centers_1 = edges_1[:-1] + np.diff(edges_1) / 2 + +hist_2, edges_2 = np.histogram(img2) +centers_2 = edges_2[:-1] + np.diff(edges_2) / 2 + +# figure size in pixels +size = (700, 560) + +# a rect is (x, y, width, height) +# here it is defined in absolute pixels +rects = [ + (0, 0, 200, 280), # for image1 + (0, 280, 200, 280), # for image2 + (200, 0, 500, 280), # for image1 histogram + (200, 280, 500, 280), # for image2 histogram +] + +# create a figure using the rects and size +# also give each subplot a name +figure = fpl.Figure( + rects=rects, + names=["astronaut image", "wikkie image", "astronaut histogram", "wikkie histogram"], + size=size +) + +# add image to the corresponding subplots +figure["astronaut image"].add_image(img1) +figure["wikkie image"].add_image(img2) + +# add histogram to the corresponding subplots +figure["astronaut histogram"].add_line(np.column_stack([centers_1, hist_1])) +figure["wikkie histogram"].add_line(np.column_stack([centers_2, hist_2])) + + +for subplot in figure: + if "image" in subplot.name: + # remove axes from image subplots to reduce clutter + subplot.axes.visible = False + continue + + # don't maintain aspect ratio for the histogram subplots + subplot.camera.maintain_aspect = False + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() From d8b89218b0a8517102706757b8cea231bfa73dec Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 03:56:32 -0400 Subject: [PATCH 42/82] black --- fastplotlib/layouts/_engine.py | 24 ++++++++++++++++++------ fastplotlib/layouts/_frame.py | 29 ++++++++++++++++++++++------- fastplotlib/layouts/_subplot.py | 2 +- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index baaa35b42..eaf76aea7 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -98,7 +98,9 @@ def _highlight_plane(self, subplot: Subplot, ev): # reset color of previous focus if self._subplot_focus is not None: - self._subplot_focus.frame.plane.material.color = subplot.frame.plane_color.idle + self._subplot_focus.frame.plane.material.color = ( + subplot.frame.plane_color.idle + ) self._subplot_focus = subplot ev.target.material.color = subplot.frame.plane_color.highlight @@ -177,11 +179,15 @@ def _new_extent_from_delta(self, delta: tuple[int, int]) -> np.ndarray: if subplot.frame.rect_manager.overlaps(new_extent): # we have an overlap, need to ignore one or more deltas # ignore x - if not subplot.frame.rect_manager.is_left_of(x0) or not subplot.frame.rect_manager.is_right_of(x1): + if not subplot.frame.rect_manager.is_left_of( + x0 + ) or not subplot.frame.rect_manager.is_right_of(x1): new_extent[:2] = self._active_subplot.frame.extent[:2] # ignore y - if not subplot.frame.rect_manager.is_above(y0) or not subplot.frame.rect_manager.is_below(y1): + if not subplot.frame.rect_manager.is_above( + y0 + ) or not subplot.frame.rect_manager.is_below(y1): new_extent[2:] = self._active_subplot.frame.extent[2:] # make sure all vals are non-negative @@ -292,7 +298,9 @@ def set_extent(self, subplot: Subplot, extent: tuple | list | np.ndarray): continue if s.frame.rect_manager.overlaps(extent): - raise ValueError(f"Given extent: {extent} overlaps with another subplot.") + raise ValueError( + f"Given extent: {extent} overlaps with another subplot." + ) class GridLayout(FlexLayout): @@ -338,7 +346,11 @@ def remove_column(self): raise NotImplementedError("Not yet implemented") def add_subplot(self): - raise NotImplementedError("Not implemented for GridLayout which is an auto layout manager") + raise NotImplementedError( + "Not implemented for GridLayout which is an auto layout manager" + ) def remove_subplot(self, subplot): - raise NotImplementedError("Not implemented for GridLayout which is an auto layout manager") + raise NotImplementedError( + "Not implemented for GridLayout which is an auto layout manager" + ) diff --git a/fastplotlib/layouts/_frame.py b/fastplotlib/layouts/_frame.py index 83829e15d..46a2cb7ee 100644 --- a/fastplotlib/layouts/_frame.py +++ b/fastplotlib/layouts/_frame.py @@ -104,7 +104,17 @@ class Frame: action=(0.1, 0.1, 0.2), # dark gray-blue ) - def __init__(self, viewport, rect, extent, resizeable, title, docks, toolbar_visible, canvas_rect): + def __init__( + self, + viewport, + rect, + extent, + resizeable, + title, + docks, + toolbar_visible, + canvas_rect, + ): self.viewport = viewport self.docks = docks self._toolbar_visible = toolbar_visible @@ -112,9 +122,7 @@ def __init__(self, viewport, rect, extent, resizeable, title, docks, toolbar_vis if rect is not None: self._rect_manager = RectManager(*rect, canvas_rect) elif extent is not None: - self._rect_manager = RectManager.from_extent( - extent, canvas_rect - ) + self._rect_manager = RectManager.from_extent(extent, canvas_rect) else: raise ValueError("Must provide `rect` or `extent`") @@ -165,7 +173,7 @@ def __init__(self, viewport, rect, extent, resizeable, title, docks, toolbar_vis self._world_object = pygfx.Group() self._world_object.add(*wobjects) - + @property def rect_manager(self) -> RectManager: return self._rect_manager @@ -240,7 +248,7 @@ def get_render_rect(self) -> tuple[float, float, float, float]: w -= 2 # subtract 2, so we get a 1 pixel edge on both sides y = ( - y + 4 + self._title_graphic.font_size + 4 + y + 4 + self._title_graphic.font_size + 4 ) # add 4 pixels above and below title for better spacing if self.toolbar_visible: @@ -253,7 +261,14 @@ def get_render_rect(self) -> tuple[float, float, float, float]: # adjust for the 4 pixels from the line above # also give space for resize handler if imgui toolbar is not present - h = h - 4 - self._title_graphic.font_size - toolbar_space - 4 - resize_handle_space + h = ( + h + - 4 + - self._title_graphic.font_size + - toolbar_space + - 4 + - resize_handle_space + ) return x, y, w, h diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index a9a801cac..c37df70fd 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -98,7 +98,7 @@ def __init__( title=name, docks=self.docks, toolbar_visible=toolbar_visible, - canvas_rect=parent.get_pygfx_render_area() + canvas_rect=parent.get_pygfx_render_area(), ) @property From 14cd6b0abe0210755d4eb78ac1bc5b5ec154eb5d Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 04:11:45 -0400 Subject: [PATCH 43/82] rename --- fastplotlib/layouts/_figure.py | 6 +++--- fastplotlib/layouts/_imgui_figure.py | 2 +- fastplotlib/layouts/_subplot.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 350807ad9..66789055e 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -151,7 +151,7 @@ def __init__( canvas, renderer, canvas_kwargs={"size": size} ) - canvas.add_event_handler(self._reset_layout, "resize") + canvas.add_event_handler(self._fpl_reset_layout, "resize") if isinstance(cameras, str): # create the array representing the views for each subplot in the grid @@ -551,7 +551,7 @@ def show( elif self.canvas.__class__.__name__ == "OffscreenRenderCanvas": # for test and docs gallery screenshots - self._reset_layout() + self._fpl_reset_layout() for subplot in self: subplot.axes.update_using_camera() @@ -722,7 +722,7 @@ def export(self, uri: str | Path | bytes, **kwargs): def open_popup(self, *args, **kwargs): warn("popups only supported by ImguiFigure") - def _reset_layout(self, *ev): + def _fpl_reset_layout(self, *ev): """set the viewport rects for all subplots, *ev argument is not used, exists because of renderer resize event""" self.layout.canvas_resized(self.get_pygfx_render_area()) diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index 3a73afc58..4ded32d24 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -168,7 +168,7 @@ def add_gui(self, gui: EdgeWindow): self.guis[location] = gui - self._reset_layout() + self._fpl_reset_layout() def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: """ diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index c37df70fd..d369b3658 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -206,7 +206,7 @@ def size(self) -> int: @size.setter def size(self, s: int): self._size = s - self.parent.reset_viewport() + self.get_figure()._fpl_reset_layout() def _render(self): if self.size == 0: From e95eaa7855de1515e356a869f6d41a0a3769a9ec Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 04:28:08 -0400 Subject: [PATCH 44/82] update test utils --- examples/tests/test_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index d5f3e8ab9..7fbd32e2f 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -103,7 +103,7 @@ def test_example_screenshots(module, force_offscreen): # hacky but it works for now example.figure.imgui_renderer.render() - example.figure._set_viewport_rects() + example.figure._fpl_reset_layout() # render each subplot for subplot in example.figure: subplot.viewport.render(subplot.scene, subplot.camera) From 2c91851b18dfde9923cd6a1bb7914905661e0ef3 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 04:48:58 -0400 Subject: [PATCH 45/82] update nb test utils --- examples/notebooks/nb_test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/notebooks/nb_test_utils.py b/examples/notebooks/nb_test_utils.py index f1505f98a..9d99e3be3 100644 --- a/examples/notebooks/nb_test_utils.py +++ b/examples/notebooks/nb_test_utils.py @@ -102,7 +102,7 @@ def plot_test(name, fig: fpl.Figure): # hacky but it works for now fig.imgui_renderer.render() - fig._set_viewport_rects() + fig._fpl_reset_layout() # render each subplot for subplot in fig: subplot.viewport.render(subplot.scene, subplot.camera) From cc005f30a67d5e60c8b57abd706c7e80f859d73e Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 05:15:04 -0400 Subject: [PATCH 46/82] update ground truths --- examples/screenshots/gridplot.png | 4 ++-- examples/screenshots/gridplot_non_square.png | 4 ++-- examples/screenshots/gridplot_viewports_check.png | 4 ++-- examples/screenshots/heatmap.png | 4 ++-- examples/screenshots/image_cmap.png | 4 ++-- examples/screenshots/image_rgb.png | 4 ++-- examples/screenshots/image_rgbvminvmax.png | 4 ++-- examples/screenshots/image_simple.png | 4 ++-- examples/screenshots/image_small.png | 4 ++-- examples/screenshots/image_vminvmax.png | 4 ++-- examples/screenshots/image_widget.png | 4 ++-- examples/screenshots/image_widget_grid.png | 4 ++-- examples/screenshots/image_widget_imgui.png | 4 ++-- examples/screenshots/image_widget_single_video.png | 4 ++-- examples/screenshots/image_widget_videos.png | 4 ++-- examples/screenshots/image_widget_viewports_check.png | 4 ++-- examples/screenshots/imgui_basic.png | 4 ++-- examples/screenshots/line.png | 4 ++-- examples/screenshots/line_cmap.png | 4 ++-- examples/screenshots/line_cmap_more.png | 4 ++-- examples/screenshots/line_collection.png | 4 ++-- examples/screenshots/line_collection_cmap_values.png | 4 ++-- .../screenshots/line_collection_cmap_values_qualitative.png | 4 ++-- examples/screenshots/line_collection_colors.png | 4 ++-- examples/screenshots/line_collection_slicing.png | 4 ++-- examples/screenshots/line_colorslice.png | 4 ++-- examples/screenshots/line_dataslice.png | 4 ++-- examples/screenshots/line_stack.png | 4 ++-- .../screenshots/linear_region_selectors_match_offsets.png | 4 ++-- examples/screenshots/no-imgui-gridplot.png | 4 ++-- examples/screenshots/no-imgui-gridplot_non_square.png | 4 ++-- examples/screenshots/no-imgui-gridplot_viewports_check.png | 4 ++-- examples/screenshots/no-imgui-heatmap.png | 4 ++-- examples/screenshots/no-imgui-image_cmap.png | 4 ++-- examples/screenshots/no-imgui-image_rgb.png | 4 ++-- examples/screenshots/no-imgui-image_rgbvminvmax.png | 4 ++-- examples/screenshots/no-imgui-image_simple.png | 4 ++-- examples/screenshots/no-imgui-image_small.png | 4 ++-- examples/screenshots/no-imgui-image_vminvmax.png | 4 ++-- examples/screenshots/no-imgui-line.png | 4 ++-- examples/screenshots/no-imgui-line_cmap.png | 4 ++-- examples/screenshots/no-imgui-line_cmap_more.png | 4 ++-- examples/screenshots/no-imgui-line_collection.png | 4 ++-- examples/screenshots/no-imgui-line_collection_cmap_values.png | 4 ++-- .../no-imgui-line_collection_cmap_values_qualitative.png | 4 ++-- examples/screenshots/no-imgui-line_collection_colors.png | 4 ++-- examples/screenshots/no-imgui-line_collection_slicing.png | 4 ++-- examples/screenshots/no-imgui-line_colorslice.png | 4 ++-- examples/screenshots/no-imgui-line_dataslice.png | 4 ++-- examples/screenshots/no-imgui-line_stack.png | 4 ++-- .../no-imgui-linear_region_selectors_match_offsets.png | 4 ++-- examples/screenshots/no-imgui-scatter_cmap_iris.png | 4 ++-- examples/screenshots/no-imgui-scatter_colorslice_iris.png | 4 ++-- examples/screenshots/no-imgui-scatter_dataslice_iris.png | 4 ++-- examples/screenshots/no-imgui-scatter_iris.png | 4 ++-- examples/screenshots/no-imgui-scatter_size.png | 4 ++-- examples/screenshots/right_click_menu.png | 3 +++ examples/screenshots/scatter_cmap_iris.png | 4 ++-- examples/screenshots/scatter_colorslice_iris.png | 4 ++-- examples/screenshots/scatter_dataslice_iris.png | 4 ++-- examples/screenshots/scatter_iris.png | 4 ++-- examples/screenshots/scatter_size.png | 4 ++-- 62 files changed, 125 insertions(+), 122 deletions(-) create mode 100644 examples/screenshots/right_click_menu.png diff --git a/examples/screenshots/gridplot.png b/examples/screenshots/gridplot.png index 1a222affd..08e6d6b78 100644 --- a/examples/screenshots/gridplot.png +++ b/examples/screenshots/gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8de769538bb435b71b33e038998b2bafa340c635211c0dfc388c7a5bf55fd36d -size 286794 +oid sha256:6f424ec68dbc0761566cd147f3bf5b8f15e4126c3b30b2ff47b6fb48f04d512a +size 252269 diff --git a/examples/screenshots/gridplot_non_square.png b/examples/screenshots/gridplot_non_square.png index 45d71abb2..781de8749 100644 --- a/examples/screenshots/gridplot_non_square.png +++ b/examples/screenshots/gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92f55da7e2912a68e69e212b31df760f27e72253ec234fe1dd5b5463b60061b3 -size 212647 +oid sha256:9ac9ee6fd1118a06a1f0de4eee73e7b6bee188c533da872c5cbaf7119114414f +size 194385 diff --git a/examples/screenshots/gridplot_viewports_check.png b/examples/screenshots/gridplot_viewports_check.png index 050067e22..b1faf9b69 100644 --- a/examples/screenshots/gridplot_viewports_check.png +++ b/examples/screenshots/gridplot_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:250959179b0f998e1c586951864e9cbce3ac63bf6d2e12a680a47b9b1be061a1 -size 46456 +oid sha256:67dd50d61a0caaf563d95110f99fa24c567ddd778a697715247d697a1b5bb1ac +size 46667 diff --git a/examples/screenshots/heatmap.png b/examples/screenshots/heatmap.png index a63eb5ec8..defcca301 100644 --- a/examples/screenshots/heatmap.png +++ b/examples/screenshots/heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f2f0699e01eb12c44a2dbefd1d8371b86b3b3456b28cb5f1850aed44c13f412 -size 94505 +oid sha256:0789d249cb4cfad21c9f1629721ade26ed734e05b1b13c3a5871793f6271362b +size 91831 diff --git a/examples/screenshots/image_cmap.png b/examples/screenshots/image_cmap.png index 6f7081b03..0301d2ed4 100644 --- a/examples/screenshots/image_cmap.png +++ b/examples/screenshots/image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1482ce72511bc4f815825c29fabac5dd0f2586ac4c827a220a5cecb1162be4d -size 210019 +oid sha256:d2bbb79716fecce08479fbe7977565daccadf4688c8a99e155db297ecce4c484 +size 199979 diff --git a/examples/screenshots/image_rgb.png b/examples/screenshots/image_rgb.png index 88beb7df3..11129ceaa 100644 --- a/examples/screenshots/image_rgb.png +++ b/examples/screenshots/image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8210ad8d1755f7819814bdaaf236738cdf1e9a0c4f77120aca4968fcd8aa8a7a -size 239431 +oid sha256:23024936931651cdf4761f2cafcd8002bb12ab86e9efb13ddc99a9bf659c3935 +size 226879 diff --git a/examples/screenshots/image_rgbvminvmax.png b/examples/screenshots/image_rgbvminvmax.png index f3ef59d84..afe4de6f7 100644 --- a/examples/screenshots/image_rgbvminvmax.png +++ b/examples/screenshots/image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ebbcc4a2e83e9733eb438fe2341f77c86579421f3fa96b6a49e94073c0ffd32 -size 48270 +oid sha256:2fb9cd6d32813df6a9e3bf183f73cb69fdb61d290d7f2a4cc223ab34301351a1 +size 50231 diff --git a/examples/screenshots/image_simple.png b/examples/screenshots/image_simple.png index 0c7e011f4..702a1ac5c 100644 --- a/examples/screenshots/image_simple.png +++ b/examples/screenshots/image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44bc2d1fd97921fef0be45424f21513d5d978b807db8cf148dfc59c07f6e292f -size 211333 +oid sha256:b3eb6f03364226e9f1aae72f6414ad05b0239a15c2a0fbcd71d3718fee477e2c +size 199468 diff --git a/examples/screenshots/image_small.png b/examples/screenshots/image_small.png index 41a4a240e..d17cb7ab2 100644 --- a/examples/screenshots/image_small.png +++ b/examples/screenshots/image_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:079ee6254dc995cc5fc8c20ff1c00cb0899f21ba2d5d1a4dc0d020c3a71902c4 -size 13022 +oid sha256:2dcfc7b8a964db9a950bf4d3217fb171d081251b107977f9acd612fcd5fb0be1 +size 14453 diff --git a/examples/screenshots/image_vminvmax.png b/examples/screenshots/image_vminvmax.png index f3ef59d84..afe4de6f7 100644 --- a/examples/screenshots/image_vminvmax.png +++ b/examples/screenshots/image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ebbcc4a2e83e9733eb438fe2341f77c86579421f3fa96b6a49e94073c0ffd32 -size 48270 +oid sha256:2fb9cd6d32813df6a9e3bf183f73cb69fdb61d290d7f2a4cc223ab34301351a1 +size 50231 diff --git a/examples/screenshots/image_widget.png b/examples/screenshots/image_widget.png index af248dd3e..23d34ae50 100644 --- a/examples/screenshots/image_widget.png +++ b/examples/screenshots/image_widget.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2ae1938c5e7b742fb2dac0336877028f6ece26cd80e84f309195a55601025cb -size 197495 +oid sha256:220ebb5286b48426f9457b62d6e7f9fe61b5a62b8874c7e010e07e146ae205a5 +size 184633 diff --git a/examples/screenshots/image_widget_grid.png b/examples/screenshots/image_widget_grid.png index e0f0ff5c8..45bc70726 100644 --- a/examples/screenshots/image_widget_grid.png +++ b/examples/screenshots/image_widget_grid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eeb5b86e7c15dfe2e71267453426930200223026f72156f34ff1ccc2f9389b6e -size 253769 +oid sha256:306977f7eebdb652828ba425d73b6018e97c100f3cf8f3cbaa0244ffb6c040a3 +size 249103 diff --git a/examples/screenshots/image_widget_imgui.png b/examples/screenshots/image_widget_imgui.png index 135a0d4c4..cb165cc86 100644 --- a/examples/screenshots/image_widget_imgui.png +++ b/examples/screenshots/image_widget_imgui.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e2cd0e3892377e6e2d552199391fc64aac6a02413168a5b4c5c4848f3390dec -size 166265 +oid sha256:7522a35768d013a257e3cf3b00cce626b023b169484e035f46c635efc553b0bf +size 165747 diff --git a/examples/screenshots/image_widget_single_video.png b/examples/screenshots/image_widget_single_video.png index 5d10d91a6..aa757a950 100644 --- a/examples/screenshots/image_widget_single_video.png +++ b/examples/screenshots/image_widget_single_video.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de1750c9c1c3cd28c356fb51687f4a8f00afb3cc7e365502342168fce8459d3a -size 90307 +oid sha256:5f0843f4693460ae985c1f33d84936fbcc943d0405e0893186cbee7a5765dbc0 +size 90283 diff --git a/examples/screenshots/image_widget_videos.png b/examples/screenshots/image_widget_videos.png index f0e262e24..2e289ae3c 100644 --- a/examples/screenshots/image_widget_videos.png +++ b/examples/screenshots/image_widget_videos.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23d993e0b5b6bcfe67da7aa4ceab3f06e99358b00f287b9703c4c3bff19648ba -size 169541 +oid sha256:eec22392f85db1fd375d7ffa995a2719cf86821fe3fe85913f4ab66084eccbf9 +size 290587 diff --git a/examples/screenshots/image_widget_viewports_check.png b/examples/screenshots/image_widget_viewports_check.png index 6bfbc0153..662432e59 100644 --- a/examples/screenshots/image_widget_viewports_check.png +++ b/examples/screenshots/image_widget_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27e8aaab0085d15965649f0a4b367e313bab382c13b39de0354d321398565a46 -size 99567 +oid sha256:1c4449f7e97375aa9d7fe1d00364945fc86b568303022157621de21a20d1d13e +size 93914 diff --git a/examples/screenshots/imgui_basic.png b/examples/screenshots/imgui_basic.png index 27288e38f..1ff9952a9 100644 --- a/examples/screenshots/imgui_basic.png +++ b/examples/screenshots/imgui_basic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3391b7cf02fc7bd2c73dc57214b21ceaca9a1513556b3a4725639f21588824e4 -size 36261 +oid sha256:09cc7b0680e53ae1a2689b63f9b0ed641535fcffc99443cd455cc8d9b6923229 +size 36218 diff --git a/examples/screenshots/line.png b/examples/screenshots/line.png index 492ea2ada..02603b692 100644 --- a/examples/screenshots/line.png +++ b/examples/screenshots/line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1458d472362f8d5bcef599fd64f931997a246f9e7649c80cc95f465cbd858850 -size 170243 +oid sha256:9bfaa54bde0967463413ecd2defa8ca18169d534163cc8b297879900e812fee8 +size 167012 diff --git a/examples/screenshots/line_cmap.png b/examples/screenshots/line_cmap.png index 10779fcd5..1ecc930e4 100644 --- a/examples/screenshots/line_cmap.png +++ b/examples/screenshots/line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66e64835f824d80dd7606d90530517dbc320bcc11a68393ab92c08fef3d23f5a -size 48828 +oid sha256:d0503c008f8869dcf83793c21b15169a93558988c1a5c4edfd2aa93c549d25e1 +size 49343 diff --git a/examples/screenshots/line_cmap_more.png b/examples/screenshots/line_cmap_more.png index 56e3fe8cc..4bf597e8b 100644 --- a/examples/screenshots/line_cmap_more.png +++ b/examples/screenshots/line_cmap_more.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de08452e47799d9afcadfc583e63da1c02513cf73000bd5c2649236e61ed6b34 -size 126725 +oid sha256:ab4d759dd679a2959c0fda724e7b7a1b7593d6f67ce797f08a5292dd0eb74fb1 +size 125023 diff --git a/examples/screenshots/line_collection.png b/examples/screenshots/line_collection.png index d9124daf1..382132770 100644 --- a/examples/screenshots/line_collection.png +++ b/examples/screenshots/line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50920f4bc21bb5beffe317777a20d8d09f90f3631a14df51c219814d3507c602 -size 100758 +oid sha256:b3b6b973a52f7088536a4f437be2a7f6ebb2787756f9170145a945c53e90093c +size 98950 diff --git a/examples/screenshots/line_collection_cmap_values.png b/examples/screenshots/line_collection_cmap_values.png index e04289699..c00bffdb6 100644 --- a/examples/screenshots/line_collection_cmap_values.png +++ b/examples/screenshots/line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:850e3deb2220d44f01e6366ee7cffb83085cad933a137b9838ce8c2231e7786a -size 64152 +oid sha256:45bb6652f477ab0165bf59e504c1935e5781bceea9a891fcfa9975dec92eef4b +size 64720 diff --git a/examples/screenshots/line_collection_cmap_values_qualitative.png b/examples/screenshots/line_collection_cmap_values_qualitative.png index 710cee119..662d3254d 100644 --- a/examples/screenshots/line_collection_cmap_values_qualitative.png +++ b/examples/screenshots/line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba5fefc8e1043fe0ebd926a6b8e6ab19e724205a4c13e4d7740122cfe464e38b -size 67017 +oid sha256:4e5b5cb45e78ae24d72f3cb84e482fac7bf0a98cd9b9b934444d2e67c9910d57 +size 66565 diff --git a/examples/screenshots/line_collection_colors.png b/examples/screenshots/line_collection_colors.png index 6c1d05f04..3b90e5b4c 100644 --- a/examples/screenshots/line_collection_colors.png +++ b/examples/screenshots/line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17d48f07310090b835e5cd2e6fa9c178db9af8954f4b0a9d52d21997ec229abd -size 57778 +oid sha256:4edf84af27535e4a30b48906ab3cacaeb38d073290828df3c5707620e222b4d3 +size 58635 diff --git a/examples/screenshots/line_collection_slicing.png b/examples/screenshots/line_collection_slicing.png index abb63760f..e0537a261 100644 --- a/examples/screenshots/line_collection_slicing.png +++ b/examples/screenshots/line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed0d4fdb729409d07ec9ec9e05d915a04ebb237087d266591e7f46b0838e05b3 -size 130192 +oid sha256:66933c1fa349ebb4dd69b9bf396acb8f0aeeabbf17a3b7054d1f1e038a6e04be +size 129484 diff --git a/examples/screenshots/line_colorslice.png b/examples/screenshots/line_colorslice.png index 1f100d89e..f3374e221 100644 --- a/examples/screenshots/line_colorslice.png +++ b/examples/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b2c5562f4150ec69029a4a139469b0a2524a14078b78055df40d9b487946ce5 -size 57037 +oid sha256:d654aa666ac1f4cfbf228fc4c5fbd2f68eed841c7cc6265637d5b836b918314c +size 57989 diff --git a/examples/screenshots/line_dataslice.png b/examples/screenshots/line_dataslice.png index b2f963195..6ecf63b26 100644 --- a/examples/screenshots/line_dataslice.png +++ b/examples/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c31a12afa3e66c442e370e6157ad9a5aad225b21f0f95fb6a115066b1b4f2e73 -size 68811 +oid sha256:a9b93af2028eb0186dd75d74c079d5effdb284a8677e6eec1a7fd2c8de4c8498 +size 70489 diff --git a/examples/screenshots/line_stack.png b/examples/screenshots/line_stack.png index 786f434be..9a9ad4fd6 100644 --- a/examples/screenshots/line_stack.png +++ b/examples/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcfa7c49d465ff9cfe472ee885bcc9d9a44106b82adfc151544847b95035d760 -size 121640 +oid sha256:4b6c2d1ee4c49ff5b193b5105b2794c6b5bd7a089a8a2c6fa03e09e02352aa65 +size 121462 diff --git a/examples/screenshots/linear_region_selectors_match_offsets.png b/examples/screenshots/linear_region_selectors_match_offsets.png index 9d2371403..327f14e72 100644 --- a/examples/screenshots/linear_region_selectors_match_offsets.png +++ b/examples/screenshots/linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f12310c09c4e84ea2c6f8245d1aa0ce9389a3d9637d7d4f9dc233bea173a0e3 -size 95366 +oid sha256:8fac4f439b34a5464792588b77856f08c127c0ee06fa77722818f8d6b48dd64c +size 95433 diff --git a/examples/screenshots/no-imgui-gridplot.png b/examples/screenshots/no-imgui-gridplot.png index 45571161d..7f870cf76 100644 --- a/examples/screenshots/no-imgui-gridplot.png +++ b/examples/screenshots/no-imgui-gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a27ccf2230628980d16ab22a17df64504268da35a27cd1adb44102e64df033af -size 329247 +oid sha256:b31f2002053b5934ae78393214e67717d10bd567e590212eaff4062440657acd +size 292558 diff --git a/examples/screenshots/no-imgui-gridplot_non_square.png b/examples/screenshots/no-imgui-gridplot_non_square.png index f8c307c22..e08d64805 100644 --- a/examples/screenshots/no-imgui-gridplot_non_square.png +++ b/examples/screenshots/no-imgui-gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58f50c4fc1b00c9e78c840193d1e15d008b9fe1e7f2a3d8b90065be91e2178f5 -size 236474 +oid sha256:c9ef00db82a3559b4d7c77b68838f5876f98a2b9e80ef9ecb257f32c62161b5e +size 216512 diff --git a/examples/screenshots/no-imgui-gridplot_viewports_check.png b/examples/screenshots/no-imgui-gridplot_viewports_check.png index 8dea071d0..2a8c0dc6f 100644 --- a/examples/screenshots/no-imgui-gridplot_viewports_check.png +++ b/examples/screenshots/no-imgui-gridplot_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cda256658e84b14b48bf5151990c828092ff461f394fb9e54341ab601918aa1 -size 45113 +oid sha256:6818a7c8bdb29567bb09cfe00acaa6872a046d4d35a87ef2be7afa06c2a8a089 +size 44869 diff --git a/examples/screenshots/no-imgui-heatmap.png b/examples/screenshots/no-imgui-heatmap.png index 3d1cf5ef2..e91d06c4f 100644 --- a/examples/screenshots/no-imgui-heatmap.png +++ b/examples/screenshots/no-imgui-heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fac55efd9339b180b9e34d5cf244c473d6439e57e34f272c1a7e59183f1afa2 -size 98573 +oid sha256:875c15e74e7ea2eaa6b00ddbdd80b4775ecb1fe0002a5122371d49f975369cce +size 95553 diff --git a/examples/screenshots/no-imgui-image_cmap.png b/examples/screenshots/no-imgui-image_cmap.png index 6c565ca2b..2d42899fc 100644 --- a/examples/screenshots/no-imgui-image_cmap.png +++ b/examples/screenshots/no-imgui-image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82f7176a61e2c6953c22171bea561845bb79cb8179d76b20eef2b2cc475bbb23 -size 237327 +oid sha256:2b43bd64ceec8c5c1287a2df57abf7bd148955d6ba97a425b32ae53bad03a051 +size 216050 diff --git a/examples/screenshots/no-imgui-image_rgb.png b/examples/screenshots/no-imgui-image_rgb.png index 355238724..6be5205ac 100644 --- a/examples/screenshots/no-imgui-image_rgb.png +++ b/examples/screenshots/no-imgui-image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fce532d713d2c664eb3b676e0128060ebf17241387134812b490d3ad398d42c2 -size 269508 +oid sha256:42516cd0719d5b33ec32523dd2efe7874398bac6d0aecb5163ff1cb5c105135f +size 244717 diff --git a/examples/screenshots/no-imgui-image_rgbvminvmax.png b/examples/screenshots/no-imgui-image_rgbvminvmax.png index 6282f2438..48d8fff95 100644 --- a/examples/screenshots/no-imgui-image_rgbvminvmax.png +++ b/examples/screenshots/no-imgui-image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42e01469f0f7da37d3c1c90225bf7c03c44badd1f3612ac9bf88eaed5eeb6850 -size 50145 +oid sha256:7f8a99a9172ae5edf98f0d189455fad2074a99f2280c9352675bab8d4c0e3491 +size 50751 diff --git a/examples/screenshots/no-imgui-image_simple.png b/examples/screenshots/no-imgui-image_simple.png index d00a166ce..1e4487757 100644 --- a/examples/screenshots/no-imgui-image_simple.png +++ b/examples/screenshots/no-imgui-image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8bb29f192617b9dde2490ce36c69bd8352b6ba5d068434bc53edaad91871356 -size 237960 +oid sha256:3cfa6469803f44a682c9ce7337ae265a8d60749070991e6f3a723eb37c5a9a23 +size 215410 diff --git a/examples/screenshots/no-imgui-image_small.png b/examples/screenshots/no-imgui-image_small.png index aca14cd69..3613a8139 100644 --- a/examples/screenshots/no-imgui-image_small.png +++ b/examples/screenshots/no-imgui-image_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1ea4bcf76158169bc06973457ea09997c13ecd4a91e6e634566beb31348ef68 -size 13194 +oid sha256:17ccf0014c7ba7054440e3daf8d4e2a397e9013d1aea804c40dc7302dad4171e +size 13327 diff --git a/examples/screenshots/no-imgui-image_vminvmax.png b/examples/screenshots/no-imgui-image_vminvmax.png index 6282f2438..48d8fff95 100644 --- a/examples/screenshots/no-imgui-image_vminvmax.png +++ b/examples/screenshots/no-imgui-image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42e01469f0f7da37d3c1c90225bf7c03c44badd1f3612ac9bf88eaed5eeb6850 -size 50145 +oid sha256:7f8a99a9172ae5edf98f0d189455fad2074a99f2280c9352675bab8d4c0e3491 +size 50751 diff --git a/examples/screenshots/no-imgui-line.png b/examples/screenshots/no-imgui-line.png index 29610c612..cdc24e382 100644 --- a/examples/screenshots/no-imgui-line.png +++ b/examples/screenshots/no-imgui-line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:709458b03d535bcf407fdae1720ccdcd11a5f79ccf673e85c7e64c5748f6d25e -size 173422 +oid sha256:d3952cf9b0c9d008a885dc4abb3aeaaed6fd94a5db05ba83c6f4c4c76fe6e925 +size 171519 diff --git a/examples/screenshots/no-imgui-line_cmap.png b/examples/screenshots/no-imgui-line_cmap.png index 9340e191e..4f2bbba43 100644 --- a/examples/screenshots/no-imgui-line_cmap.png +++ b/examples/screenshots/no-imgui-line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69426f5aac61e59a08764626b2aded602e576479e652d76b6b3bf646e3218cc1 -size 48028 +oid sha256:d3c9ac8d2b8157ffd575e5ad2b2bb23b684b52403c2f4f021c52d100cfb28a83 +size 49048 diff --git a/examples/screenshots/no-imgui-line_cmap_more.png b/examples/screenshots/no-imgui-line_cmap_more.png index f0cea4ec1..8125be49f 100644 --- a/examples/screenshots/no-imgui-line_cmap_more.png +++ b/examples/screenshots/no-imgui-line_cmap_more.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df9a2ef9d54b417e0387116eb6e6215c54b7c939867d0d62c768768baae27e5f -size 129510 +oid sha256:5ddd88200aa824d4e05ba3f94fdb4216a1e7c7137b202cd8fb47997453dfd5a6 +size 126830 diff --git a/examples/screenshots/no-imgui-line_collection.png b/examples/screenshots/no-imgui-line_collection.png index ca74d3362..a31cf55fe 100644 --- a/examples/screenshots/no-imgui-line_collection.png +++ b/examples/screenshots/no-imgui-line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90f281301e8b23a22a5333e7b34316475907ac25ffc9a23b7395b7431c965343 -size 106518 +oid sha256:7d807f770c118e668c6bda1919856d7804f716a2bf95a5ae060345df1cd2b3c7 +size 102703 diff --git a/examples/screenshots/no-imgui-line_collection_cmap_values.png b/examples/screenshots/no-imgui-line_collection_cmap_values.png index df237aa1b..c909c766f 100644 --- a/examples/screenshots/no-imgui-line_collection_cmap_values.png +++ b/examples/screenshots/no-imgui-line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f5a7257d121a15a8a35ca6e9c70de9d6fbb4977221c840dd34e25e67136f4ea -size 67209 +oid sha256:2e8612de5c3ee252ce9c8cc8afd5bd6075d5e242e8a93cd025e28ec82526120f +size 64698 diff --git a/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png b/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png index 0347f7361..61d5a21d0 100644 --- a/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png +++ b/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89a7bc62495e6454ee008e15f1504211777cc01e52f303c18f6068fd38ab3c12 -size 70090 +oid sha256:7847cd4399ce5b43bda9985eb72467ad292744aaeb9e8d210dd6c86c4eb1a090 +size 67959 diff --git a/examples/screenshots/no-imgui-line_collection_colors.png b/examples/screenshots/no-imgui-line_collection_colors.png index dff4f83db..567bb4d06 100644 --- a/examples/screenshots/no-imgui-line_collection_colors.png +++ b/examples/screenshots/no-imgui-line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78b14e90e5ae1e185abb51d94ac9d99c1a4318b0ddf79c26a55e6061f22c0ed9 -size 60447 +oid sha256:15216a0900bcaef492e5d9e3380db9f28d7b7e4bd11b26eb87ce956666dcd2b1 +size 58414 diff --git a/examples/screenshots/no-imgui-line_collection_slicing.png b/examples/screenshots/no-imgui-line_collection_slicing.png index 70c343361..c9bc6d931 100644 --- a/examples/screenshots/no-imgui-line_collection_slicing.png +++ b/examples/screenshots/no-imgui-line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6b4090d3ae9e38256c9f04e17bf2499f0a35348552f62e9c8d8dc97c9e760a7 -size 132125 +oid sha256:e8d3d7813580be188766c2d0200bcbff28122758d36d0faa846b0bb4dceac654 +size 130453 diff --git a/examples/screenshots/no-imgui-line_colorslice.png b/examples/screenshots/no-imgui-line_colorslice.png index 3befac6da..fe54de5d6 100644 --- a/examples/screenshots/no-imgui-line_colorslice.png +++ b/examples/screenshots/no-imgui-line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f161ad7f351b56c988e1b27155e3963be5191dc09cbaa55615026d07df07334 -size 56338 +oid sha256:be429bf910979cf4c9483b8ae1f7aa877fde64fb6ec8a4cf32be143f282c9103 +size 57353 diff --git a/examples/screenshots/no-imgui-line_dataslice.png b/examples/screenshots/no-imgui-line_dataslice.png index 957462d09..649a9df59 100644 --- a/examples/screenshots/no-imgui-line_dataslice.png +++ b/examples/screenshots/no-imgui-line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2f737e0afd8f57c7d621197d37fcf30199086f6c083ec0d3d8e5497965e6d12 -size 67938 +oid sha256:cf873f1479cec065f0062ce58ce78ddfbd5673654aacf0ecdbd559747ae741cb +size 69381 diff --git a/examples/screenshots/no-imgui-line_stack.png b/examples/screenshots/no-imgui-line_stack.png index 26f4a3af8..3ef24e73a 100644 --- a/examples/screenshots/no-imgui-line_stack.png +++ b/examples/screenshots/no-imgui-line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dd69dc4be7a2283ec11a8427a75a2ddfe4be0cdbbdaedef3dcbf5f567c11ea7 -size 130519 +oid sha256:4b9d02719e7051c2a0e848cc828f21be52ac108c6f9be16795d1150a1e215371 +size 123674 diff --git a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png index 9871d65c1..809908432 100644 --- a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png +++ b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:747b0915eeaf5985346e3b6807a550da53b516769d2517d7c2e0f189baefef91 -size 100604 +oid sha256:303d562f1a16f6a704415072d43ca08a51e12a702292b522e0f17f397b1aee60 +size 96668 diff --git a/examples/screenshots/no-imgui-scatter_cmap_iris.png b/examples/screenshots/no-imgui-scatter_cmap_iris.png index 35812357a..0d1f8dbb0 100644 --- a/examples/screenshots/no-imgui-scatter_cmap_iris.png +++ b/examples/screenshots/no-imgui-scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74438dc47ff3fc1391b6952a52c66160fece0545de4ad40c13d3d56b2e093257 -size 59951 +oid sha256:7e197c84911cf7711d09653d6c54d7a756fbe4fe80daa84f0cf1a1d516217423 +size 60341 diff --git a/examples/screenshots/no-imgui-scatter_colorslice_iris.png b/examples/screenshots/no-imgui-scatter_colorslice_iris.png index 61812c8d7..84447c70f 100644 --- a/examples/screenshots/no-imgui-scatter_colorslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a02a21459deeca379a69b30054bebcc3739553b9d377d25b953315094e714d1a -size 35763 +oid sha256:780b680de7d3a22d2cb73a6829cad1e1066163e084b8daa9e8362f2543ba62eb +size 36881 diff --git a/examples/screenshots/no-imgui-scatter_dataslice_iris.png b/examples/screenshots/no-imgui-scatter_dataslice_iris.png index 9ef39785c..a19d66270 100644 --- a/examples/screenshots/no-imgui-scatter_dataslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21ccf85a9242f6d7a724c38797688abd804d9a565e818b81ea0c8931aa05ca4e -size 38337 +oid sha256:6b4f6635f48e047944c923ac46a9bd5b77e736f26421978ff74cd37a9677c622 +size 39457 diff --git a/examples/screenshots/no-imgui-scatter_iris.png b/examples/screenshots/no-imgui-scatter_iris.png index 91dc29397..631672504 100644 --- a/examples/screenshots/no-imgui-scatter_iris.png +++ b/examples/screenshots/no-imgui-scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ec960574580af159f3502da09f1f34e841267985edb52b89baf034c1d49125e -size 37410 +oid sha256:80cc8c1ed5276b0b8cbd5aeb3151182a73984829f889195b57442a58c3124a43 +size 38488 diff --git a/examples/screenshots/no-imgui-scatter_size.png b/examples/screenshots/no-imgui-scatter_size.png index 6fadfec4d..241e38ad5 100644 --- a/examples/screenshots/no-imgui-scatter_size.png +++ b/examples/screenshots/no-imgui-scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94b4b9d39f3d4ef2c46b6b4dd7f712ca612f31a7fc94ab5fad8015e48c637e91 -size 70290 +oid sha256:71f3db93ea28e773c708093319985fb0fe04fae9a8a78d4f4f764f0417979b72 +size 68596 diff --git a/examples/screenshots/right_click_menu.png b/examples/screenshots/right_click_menu.png new file mode 100644 index 000000000..2f1db3c1e --- /dev/null +++ b/examples/screenshots/right_click_menu.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf01e976a5bf92bfae3d9bbbcedeb93ca91968fab8b37b9815aa8d7c992869f6 +size 5446 diff --git a/examples/screenshots/scatter_cmap_iris.png b/examples/screenshots/scatter_cmap_iris.png index a887d1f99..c069d6b11 100644 --- a/examples/screenshots/scatter_cmap_iris.png +++ b/examples/screenshots/scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d6bfba80eb737099040eebce9b70e1b261720f26cc895ec4b81ca21af60471c -size 60550 +oid sha256:fad40cf8004e31f7d30f4bb552ee1c7f79a499d3bad310c0eac83396f0aabd62 +size 61193 diff --git a/examples/screenshots/scatter_colorslice_iris.png b/examples/screenshots/scatter_colorslice_iris.png index e260df642..58c2b61fe 100644 --- a/examples/screenshots/scatter_colorslice_iris.png +++ b/examples/screenshots/scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:febd4aa7240eea70b2759337cf98be31cacc1b147859bf628e929ead0153ef9c -size 36791 +oid sha256:427587ef9a73bf9c3ea6e739b61d5af7380a5488c454a9d3653019b40d569292 +size 37589 diff --git a/examples/screenshots/scatter_dataslice_iris.png b/examples/screenshots/scatter_dataslice_iris.png index e5f05bb74..ab61f0405 100644 --- a/examples/screenshots/scatter_dataslice_iris.png +++ b/examples/screenshots/scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cfbc717281c15c6d1d8fe2989770bc9c46f42052c897c2270294ad1b4b40d66 -size 39296 +oid sha256:e3dd9ad854f41386d353ca0dae689a263eff942817727e328690427e2e62e2f3 +size 40112 diff --git a/examples/screenshots/scatter_iris.png b/examples/screenshots/scatter_iris.png index 9c452d448..01bd5cacd 100644 --- a/examples/screenshots/scatter_iris.png +++ b/examples/screenshots/scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98eab41312eb42cbffdf8add0651b55e63b5c2fb5f4523e32dc51ed28a1be369 -size 38452 +oid sha256:c7978b93f7eac8176c54ed0e39178424d9cb6474c73e9013d5164d3e88d54c95 +size 39147 diff --git a/examples/screenshots/scatter_size.png b/examples/screenshots/scatter_size.png index f2f036ea4..2f6c045f3 100644 --- a/examples/screenshots/scatter_size.png +++ b/examples/screenshots/scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3522468f99c030cb27c225f009ecb4c7aafbd97cfc743cf1d07fb8d7ff8e0d4 -size 71336 +oid sha256:eb05b8378d94e16094738850dca6328caf7477c641bf474b9deae426344bc7a4 +size 70898 From 6f57644293957e81940204b2cdebc2474959acf6 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 05:15:31 -0400 Subject: [PATCH 47/82] update nb ground truths --- examples/notebooks/screenshots/nb-astronaut.png | 4 ++-- examples/notebooks/screenshots/nb-astronaut_RGB.png | 4 ++-- examples/notebooks/screenshots/nb-camera.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-movie-set_data.png | 4 ++-- .../screenshots/nb-image-widget-movie-single-0-reset.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-movie-single-0.png | 4 ++-- .../screenshots/nb-image-widget-movie-single-279.png | 4 ++-- .../nb-image-widget-movie-single-50-window-max-33.png | 4 ++-- .../nb-image-widget-movie-single-50-window-mean-13.png | 4 ++-- .../nb-image-widget-movie-single-50-window-mean-33.png | 4 ++-- .../nb-image-widget-movie-single-50-window-reset.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-movie-single-50.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-single-gnuplot2.png | 4 ++-- examples/notebooks/screenshots/nb-image-widget-single.png | 4 ++-- .../nb-image-widget-zfish-frame-50-frame-apply-gaussian.png | 4 ++-- .../nb-image-widget-zfish-frame-50-frame-apply-reset.png | 4 ++-- .../nb-image-widget-zfish-frame-50-max-window-13.png | 4 ++-- .../nb-image-widget-zfish-frame-50-mean-window-13.png | 4 ++-- .../nb-image-widget-zfish-frame-50-mean-window-5.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-zfish-frame-50.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-zfish-frame-99.png | 4 ++-- ...-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-max-window-13.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-mean-window-13.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-mean-window-5.png | 4 ++-- .../screenshots/nb-image-widget-zfish-grid-frame-50.png | 4 ++-- .../screenshots/nb-image-widget-zfish-grid-frame-99.png | 4 ++-- .../nb-image-widget-zfish-grid-init-mean-window-5.png | 4 ++-- ...b-image-widget-zfish-grid-set_data-reset-indices-false.png | 4 ++-- ...nb-image-widget-zfish-grid-set_data-reset-indices-true.png | 4 ++-- .../screenshots/nb-image-widget-zfish-init-mean-window-5.png | 4 ++-- .../nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png | 4 ++-- .../nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png | 4 ++-- .../nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png | 4 ++-- examples/notebooks/screenshots/nb-lines-3d.png | 4 ++-- examples/notebooks/screenshots/nb-lines-colors.png | 4 ++-- examples/notebooks/screenshots/nb-lines-data.png | 4 ++-- examples/notebooks/screenshots/nb-lines-underlay.png | 4 ++-- examples/notebooks/screenshots/nb-lines.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-astronaut.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-camera.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-lines-3d.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-lines-colors.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-lines-data.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-lines.png | 4 ++-- 48 files changed, 96 insertions(+), 96 deletions(-) diff --git a/examples/notebooks/screenshots/nb-astronaut.png b/examples/notebooks/screenshots/nb-astronaut.png index 32b09caf9..2370c5988 100644 --- a/examples/notebooks/screenshots/nb-astronaut.png +++ b/examples/notebooks/screenshots/nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d9e2b0479d3de1c12764b984679dba83a1876ea6a88c072789a0e06f957ca2a -size 70655 +oid sha256:0a6e8bb3c72f1be6915e8e78c9a4f269419cfb4faded16e39b5cb11d70bec247 +size 64185 diff --git a/examples/notebooks/screenshots/nb-astronaut_RGB.png b/examples/notebooks/screenshots/nb-astronaut_RGB.png index be498bb6d..2a7eac585 100644 --- a/examples/notebooks/screenshots/nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2d02877510191e951d38d03a6fe9d31f5c0c335913876c65b175c4bb1a9c0e1 -size 69942 +oid sha256:9f9f32e86018f87057435f7121b02bbe98823444babb330645bab618e1d586b7 +size 63838 diff --git a/examples/notebooks/screenshots/nb-camera.png b/examples/notebooks/screenshots/nb-camera.png index 3e9a518f9..bfe226ca4 100644 --- a/examples/notebooks/screenshots/nb-camera.png +++ b/examples/notebooks/screenshots/nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5271c2204a928185b287c73c852ffa06b708d8d6a33de09acda8d2ea734e78c5 -size 51445 +oid sha256:2964d0150b38f990a7b804e9057f99505e8c99bb04538a13137989d540704593 +size 47456 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 8c353442a..2578ad028 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:4d8563587c4f642d5e4edb34f41b569673d7ea71bcbafdb734369272776baeef -size 62316 +oid sha256:78e7e99fafc15cc6edf53cfb2e5b679623ad14e0d594e0ad615088e623be22e1 +size 60988 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png index 22c7ad73a..bb2e1ee37 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b122f0ba9bfff0b0868778f09744870238bf7b4945e57410b6aa36341eaaf4a -size 116781 +oid sha256:6c9b898259fc965452ef0b6ff53ac7fa41196826c6e27b6b5d417d33fb352051 +size 112399 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png index 22c7ad73a..bb2e1ee37 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b122f0ba9bfff0b0868778f09744870238bf7b4945e57410b6aa36341eaaf4a -size 116781 +oid sha256:6c9b898259fc965452ef0b6ff53ac7fa41196826c6e27b6b5d417d33fb352051 +size 112399 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png index 84e2514d0..1841cd237 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcc5092f35c881da4a9b9f3c216fb608b8dfc27a791b83e0d5184ef3973746cf -size 139375 +oid sha256:b9cbc2a6916c7518d40812a13276270eb1acfc596f3e6e02e98a6a5185da03a4 +size 132971 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png index 075116ff4..6cc1821fa 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fabd9d52ae2815ae883a4c8c8a8b1385c0824e0212347896a09eb3600c29430 -size 124238 +oid sha256:070748e90bd230a01d3ae7c6d6487815926b0158888a52db272356dc8b0a89d7 +size 119453 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png index 216ae2b9e..3865aef93 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86ad31cab3aefa24a1c4c0adc2033cbc9fa594e9cf8ab8e4a6ff0a3630bb7896 -size 109041 +oid sha256:b24450ccf1f8cf902b8e37e73907186f37a6495f227dcbd5ec53f75c52125f56 +size 105213 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png index 99302d4e6..025086930 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ebf4e875199c7e682dc15aa03a36ea9f111618853a94076064b055bf6ce788e -size 101209 +oid sha256:3dfc8e978eddf08d1ed32e16fbf93c037ccdf5f7349180dcda54578a8c9e1a18 +size 97359 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png index 3bb5081f0..5ff5052b0 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8dbf6b76818315e40d9d4cc97807c4276c27e7a9a09d2643f74adf701ef1cdc -size 123136 +oid sha256:00130242d3f199926604df16dda70a062071f002566a8056e4794805f29adfde +size 118044 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png index 3bb5081f0..5ff5052b0 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8dbf6b76818315e40d9d4cc97807c4276c27e7a9a09d2643f74adf701ef1cdc -size 123136 +oid sha256:00130242d3f199926604df16dda70a062071f002566a8056e4794805f29adfde +size 118044 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index 48ab5d6fe..e8c02adfe 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c65e2dc4276393278ab769706f22172fd71e38eeb3c9f4d70fa51de31820f1d1 -size 234012 +oid sha256:8c8562f8e1178cf21af98af635006c64010f3c5fc615533d1df8c49479232843 +size 217693 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index 5e1cb8cc1..8de4099fb 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d4e4edf1429a135bafb7c1c927ea87f78a93fb5f3e0cbee2fb5c156af88d5a0 -size 220490 +oid sha256:5c9bae3c9c5521a4054288be7ae548204fc7b0eafbc3e99cb6b649e0be797169 +size 207176 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index ec2911374..13297e09f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39adce1898e5b00ccf9d8792bd4e76f2da2591a8c3f6e201a5c2af1f814d37cb -size 58692 +oid sha256:70c7738ed303f5a3e19271e8dfc12ab857a6f3aff767bdbecb485b763a09913e +size 55584 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index ae72c8175..b8307bc44 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d50b960c66acc6672aaeb6a97f6a69aad14f9e54060c3702679d6a5bf2b70e78 -size 70582 +oid sha256:66a435e45dc4643135633115af2eeaf70761e408a94d70d94d80c14141574528 +size 69343 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index 66f9136dc..d6237dc9f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d244a8a91d04380f2ebe255b2b56b3be5249c0a382821877710cae6bdaa2d414 -size 128643 +oid sha256:731f225fa2de3457956b2095d1cc539734983d041b13d6ad1a1f9d8e7ebfa4bc +size 115239 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index 230e71c0f..ecf63a369 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24c991457b8b081ee271cbdb03821ea1024c8340db92340e0e445bf3c70aba40 -size 97903 +oid sha256:7e2d70159ac47c004acb022b3a669e7bd307299ddd590b83c08854b0dba27b70 +size 93885 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index a355670a0..e7106fae9 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdd62a9bd1ca4f1ff110a30fb4064d778f02120295a3e3d30552e06892146e40 -size 93658 +oid sha256:1756783ab90435b46ded650033cf29ac36d2b4380744bf312caa2813267f7f38 +size 89813 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index c47545ccb..ddd4f85ca 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db7e2cf15ff3ce61b169b114c630e2339c1c6b5c687c580e1ee6964785df6790 -size 74844 +oid sha256:a35e2e4b892b55f5d2500f895951f6a0289a2df3b69cf12f59409bbc091d1caf +size 72810 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index 69ef49149..d9971c3fd 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64d2d3fd91ac8e10736add5a82a312927ae6f976119cfa2aaa1fc1e008bcf6f1 -size 66038 +oid sha256:3bdb0ed864c8a6f2118cfe0d29476f61c54576f7b8e041f3c3a895ba0a440c05 +size 65039 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index bb04d1800..6736e108c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d2a805c85e1cdf5bd2d995600b033019680ac645d7634efeaf1db7d0d00d4aa -size 79403 +oid sha256:7ae7c86bee3a30bde6cfa44e1e583e6dfd8de6bb29e7c86cea9141ae30637b4a +size 80627 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index 5b1a4a8da..dce99223b 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:440623bb4588994c4f52f17e027af29d1f2d5d330c5691630fd2acb9e38f8a25 -size 99033 +oid sha256:b51a5d26f2408748e59e3ee481735694f8f376539b50deb2b5c5a864b7de1079 +size 105581 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index bd72160dd..cdea3673d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ee56adf8f2a516ef74a799e9e503f980c36c4dfb41f9ff6d8168cfcf65ad092 -size 132745 +oid sha256:e854f7f2fdaeeac6c8358f94a33698b5794c0f6c55b240d384e8c6d51fbfb0ff +size 143301 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index 438d1e2d4..25a2fa53e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de4733b82c2e77baa659582401eff0c70ec583c50462b33bcbfd2bb00ceaa517 -size 102959 +oid sha256:c8c8d3c59c145a4096deceabc71775a03e5e121e82509590787c768944d155bd +size 110744 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index ee081c6df..00a4a1fd2 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6107f108b5a86ba376c53f5e207841c01a85b686100efb46e5df3565127201d2 -size 106765 +oid sha256:c4b4af7b99cad95ea3f688af8633de24b6602bd700cb244f93c28718af2e1e85 +size 114982 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index c2071c850..3b5594c64 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:caa15f6bc21a3be0f480b442414ec4b96b21cc1de2cdcb891b366692962d4ef8 -size 100753 +oid sha256:6d28a4be4c76d5c0da5f5767b169acf7048a268b010f33f96829a5de7f06fd7d +size 107477 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index 3d90fd77a..239237b45 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e23288d695a5a91188b285f6a0a2c9f0643dd19f3d6dedb56f4389f44ed1f44 -size 98621 +oid sha256:30dba982c9a605a7a3c0f2fa6d8cdf0df4160b2913a95b26ffdb6b04ead12add +size 104603 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index 3fd5688d9..0745a4d4a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b4e1bb60466d7553b4d1afc14015b7c4edc6e79c724c0afb5acd123a10093d0 -size 105541 +oid sha256:e431229806ee32a78fb9313a09af20829c27799798232193feab1723b66b1bca +size 112646 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index 048078520..498b19cb7 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33ce1260b4715b3d28ba28d0ad4c5eb94c9997bdc1676ff6208121e789e168a5 -size 99287 +oid sha256:a8e899b48881e3eb9200cc4e776db1f865b0911c340c06d4009b3ae12aa1fc85 +size 105421 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index ade8fb483..369168141 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e08f4e4cb3330fbbbf827af56c02039af3b293036c7676f2a87c309ad07f2f6 -size 99759 +oid sha256:93933e7ba5f791072df2934c94a782e39ed97f7db5b55c5d71c8c5bbfc69d800 +size 106360 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index 14d9e8448..b62721be2 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3aad82db14f8100f669d2ad36b5bc3973b7c12457adfdd73adbc81c759338f7b -size 80964 +oid sha256:bf38b2af1ceb372cd0949d42c027acb5fcc4c6b9a8f38c5aacdce1cd14e290fe +size 78533 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png index af04a6f73..76ed01a7c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e40559eea03790315718c55b4ec4976aacb97a2f07bcdc49d917c044745687c2 -size 117144 +oid sha256:ff462d24820f0bdd509e58267071fa956b5c863b8b8d66fea061c5253b7557cf +size 113926 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png index 7f530e554..d9a593ee7 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:414ebe9a0b2bc4eb1caa4b4aeef070955c662bb691899c4b2046be3e2ca821e3 -size 113649 +oid sha256:2b8fd14f8e8a90c3cd3fbb84a00d50b1b826b596d64dfae4a5ea1bab0687d906 +size 110829 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png index e2f6b8318..cf10c6d42 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea6d0c4756db434af6e257b7cd809f1d49089eca6b9eae9e347801e20b175686 -size 113631 +oid sha256:d88c64b716d19a3978bd60f8d75ffe09e022183381898fa1c48b77598be8fb7c +size 111193 diff --git a/examples/notebooks/screenshots/nb-lines-3d.png b/examples/notebooks/screenshots/nb-lines-3d.png index 2e26a8cd7..fb84ef21a 100644 --- a/examples/notebooks/screenshots/nb-lines-3d.png +++ b/examples/notebooks/screenshots/nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:857eb528b02fd7dd4b9f46ce1e65942066736f1bdf5271db141d73a0abab82b0 -size 19457 +oid sha256:c70c01b3ade199864df227a44fb28a53626df3beecee722a7b782c9a9f4658d8 +size 19907 diff --git a/examples/notebooks/screenshots/nb-lines-colors.png b/examples/notebooks/screenshots/nb-lines-colors.png index 1e13983f3..ab221d83f 100644 --- a/examples/notebooks/screenshots/nb-lines-colors.png +++ b/examples/notebooks/screenshots/nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6681a1e5658c1f2214217dcb7321cad89c7a0a3fd7919296a1069f27f1a7ee92 -size 35381 +oid sha256:3b238b085eddb664ff56bd265423d85b35fc70769ebec050b27fefa8fe6380de +size 35055 diff --git a/examples/notebooks/screenshots/nb-lines-data.png b/examples/notebooks/screenshots/nb-lines-data.png index a7e8287ef..44b142f55 100644 --- a/examples/notebooks/screenshots/nb-lines-data.png +++ b/examples/notebooks/screenshots/nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:043d8d9cd6dfc7627a6ccdb5810efd4b1a15e8880a4e30c0f558ae4d67c2f470 -size 42410 +oid sha256:4df736ec3ea90478930a77437949977f8e30f7d9272f65ef9f4908f2103dd11e +size 40679 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index c2908d479..f4a5b4e76 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c52ac60ffc08005d1f1fcad1b29339a89a0f31b58c9ca692f9d93400e7c8ac9e -size 48540 +oid sha256:3a8b59386015b4c1eaa85c33c7b041d566ac1ac76fbba829075e9a3af021bedf +size 46228 diff --git a/examples/notebooks/screenshots/nb-lines.png b/examples/notebooks/screenshots/nb-lines.png index f4a4d58b1..8c86b48d0 100644 --- a/examples/notebooks/screenshots/nb-lines.png +++ b/examples/notebooks/screenshots/nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cef0e2fb84e985f8d9c18f77817fb3eba31bd30b8fa4c54bb71432587909458 -size 30075 +oid sha256:823558e877830b816cc87df0776a92d5316d98a4f40e475cbf997b597c5eb8de +size 30338 diff --git a/examples/notebooks/screenshots/no-imgui-nb-astronaut.png b/examples/notebooks/screenshots/no-imgui-nb-astronaut.png index a1e524e2a..9f9e2013a 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-astronaut.png +++ b/examples/notebooks/screenshots/no-imgui-nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:915f6c4695c932dc2aa467be750e58a0435fe86fe0e0fa5a52c6065e05ec3193 -size 85456 +oid sha256:4758a94e6c066d95569515c0bff8e4c9ec383c65c5928a827550c142214df085 +size 72372 diff --git a/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png b/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png index ec3208e01..23d1bd906 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31cfa60229a4e297be507a8888e08d6950c2a7d4b323d34774c9462419272ada -size 84284 +oid sha256:fb3c72edc6f41d6f77e44bc68e7f5277525d2548d369925827c14d855dc33bbd +size 71588 diff --git a/examples/notebooks/screenshots/no-imgui-nb-camera.png b/examples/notebooks/screenshots/no-imgui-nb-camera.png index 31b60d9c0..22c70a760 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-camera.png +++ b/examples/notebooks/screenshots/no-imgui-nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:800845fae18093945ed921237c8756b1afa31ee391fe679b03c57a67929e4ba9 -size 60087 +oid sha256:6de3880cc22a8f6cdb77305e4d5be520fe92fd54a9a107bdbddf1e6f72c19262 +size 52157 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png index 35c777e6a..1a5a7b548 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4253362c0908e0d983542be3691a3d94f27a0319fb9e7183315c77891dac140 -size 23232 +oid sha256:f0e63c918aac713af2015cb85289c9451be181400834b0f60bcbb50564551f08 +size 20546 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png index b8e34aab3..cdce4bf46 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc95d6291d06ab64d142ba0048318caefa28b404bb4b31635df075dc651eaa08 -size 37276 +oid sha256:2bd481f558907ac1af97bd7ee08d58951bada758cc32467c73483fa66e4602f8 +size 36206 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png index 8f58dbc6d..8923be766 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8aa0b8303f0a69609198ea312800fc0eb98007c18d0ebc37672a9cf4f1cbaff -size 46780 +oid sha256:ea39e2651408431ad5e49af378828a41b7b377f7f0098adc8ce2c7b5e10d0234 +size 43681 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png index b33cde5a6..b6b4cf340 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:822410f43d48d12e70930b5b581bafe624ea72475d53ca0d98cdaa5649338c63 -size 51849 +oid sha256:6a8d4aba2411598ecae1b7f202fbb1a1fa7416a814b7b4c5fdd1e0e584cdb06a +size 49343 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines.png b/examples/notebooks/screenshots/no-imgui-nb-lines.png index 5d7e704ca..5d03421a4 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e3ba744fcfa43df839fddce88f79fb8d7c5eafdd22f271e6b885e09b8891072 -size 31222 +oid sha256:b2fdaf79703c475521184ab9dc948d3e817160b0162e9d88fcb20207225d0233 +size 31153 From 5fe5815a5cd464d41e9239e53e2115e1e09c6ec7 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 05:17:55 -0400 Subject: [PATCH 48/82] flex layouts examples 'as screenshot tests --- examples/flex_layouts/extent_frac_layout.py | 3 +++ examples/flex_layouts/extent_layout.py | 3 +++ examples/flex_layouts/rect_frac_layout.py | 3 +++ examples/flex_layouts/rect_layout.py | 3 +++ 4 files changed, 12 insertions(+) diff --git a/examples/flex_layouts/extent_frac_layout.py b/examples/flex_layouts/extent_frac_layout.py index 8f9709765..562c5814f 100644 --- a/examples/flex_layouts/extent_frac_layout.py +++ b/examples/flex_layouts/extent_frac_layout.py @@ -7,6 +7,9 @@ """ +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + import numpy as np import imageio.v3 as iio import fastplotlib as fpl diff --git a/examples/flex_layouts/extent_layout.py b/examples/flex_layouts/extent_layout.py index d1badd497..022ab9d5e 100644 --- a/examples/flex_layouts/extent_layout.py +++ b/examples/flex_layouts/extent_layout.py @@ -7,6 +7,9 @@ """ +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + import numpy as np import imageio.v3 as iio import fastplotlib as fpl diff --git a/examples/flex_layouts/rect_frac_layout.py b/examples/flex_layouts/rect_frac_layout.py index 96d0fd063..bb60453f1 100644 --- a/examples/flex_layouts/rect_frac_layout.py +++ b/examples/flex_layouts/rect_frac_layout.py @@ -7,6 +7,9 @@ """ +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + import numpy as np import imageio.v3 as iio import fastplotlib as fpl diff --git a/examples/flex_layouts/rect_layout.py b/examples/flex_layouts/rect_layout.py index 96bce3cca..4b8b9d607 100644 --- a/examples/flex_layouts/rect_layout.py +++ b/examples/flex_layouts/rect_layout.py @@ -7,6 +7,9 @@ """ +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + import numpy as np import imageio.v3 as iio import fastplotlib as fpl From 2b70265cb20f645f0c82bfdc9858bee28c3eb7a2 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 9 Mar 2025 05:42:04 -0400 Subject: [PATCH 49/82] accidentaly added screenshot --- examples/screenshots/right_click_menu.png | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 examples/screenshots/right_click_menu.png diff --git a/examples/screenshots/right_click_menu.png b/examples/screenshots/right_click_menu.png deleted file mode 100644 index 2f1db3c1e..000000000 --- a/examples/screenshots/right_click_menu.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cf01e976a5bf92bfae3d9bbbcedeb93ca91968fab8b37b9815aa8d7c992869f6 -size 5446 From 4e9998a010bc875bc61550a045f47a546b27d699 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Mar 2025 02:09:16 -0400 Subject: [PATCH 50/82] comments, cleanup --- fastplotlib/layouts/_figure.py | 19 +++++-- fastplotlib/layouts/_frame.py | 75 ++++++++++++++++++++++------ fastplotlib/layouts/_imgui_figure.py | 4 +- fastplotlib/layouts/_rect.py | 11 ++-- fastplotlib/layouts/_subplot.py | 23 ++------- 5 files changed, 86 insertions(+), 46 deletions(-) diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 66789055e..a234ff186 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -19,7 +19,7 @@ ) from ._utils import controller_types as valid_controller_types from ._subplot import Subplot -from ._engine import GridLayout, FlexLayout, UnderlayCamera +from ._engine import BaseLayout, GridLayout, FlexLayout, UnderlayCamera from .. import ImageGraphic @@ -52,12 +52,20 @@ def __init__( names: list | np.ndarray = None, ): """ - A grid of subplots. + Create a Figure containing Subplots. Parameters ---------- - shape: list[tuple[int, int, int, int]] | tuple[int, int], default (1, 1) - grid of shape [n_rows, n_cols] or list of bounding boxes: [x, y, width, height] (NOT YET IMPLEMENTED) + shape: tuple[int, int], default (1, 1) + shape [n_rows, n_cols] that defines a grid of subplots + + rects: list of tuples or arrays + list of rects (x, y, width, height) that define the subplots. + rects can be defined in absolute pixels or as a fraction of the canvas + + extents: list of tuples or arrays + list of extents (xmin, xmax, ymin, ymax) that define the subplots. + extents can be defined in absolute pixels or as a fraction of the canvas cameras: "2d", "3", list of "2d" | "3d", Iterable of camera instances, or Iterable of "2d" | "3d", optional | if str, one of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots @@ -95,10 +103,11 @@ def __init__( pygfx renderer instance size: (int, int), optional - starting size of canvas, default (500, 300) + starting size of canvas in absolute pixels, default (500, 300) names: list or array of str, optional subplot names + """ if rects is not None: diff --git a/fastplotlib/layouts/_frame.py b/fastplotlib/layouts/_frame.py index 46a2cb7ee..a6b4f9ef9 100644 --- a/fastplotlib/layouts/_frame.py +++ b/fastplotlib/layouts/_frame.py @@ -36,7 +36,7 @@ # wgsl shader snippet for SDF function that defines the resize handler, a lower right triangle. -sdf_wgsl_resize_handler = """ +sdf_wgsl_resize_handle = """ // hardcode square root of 2 let m_sqrt_2 = 1.4142135; @@ -49,7 +49,7 @@ class MeshMasks: - """Used set the x1, x1, y0, y1 positions of the mesh""" + """Used set the x1, x1, y0, y1 positions of the plane mesh""" x0 = np.array( [ @@ -92,12 +92,14 @@ class MeshMasks: class Frame: + # resize handle color states resize_handle_color = SelectorColorStates( idle=(0.6, 0.6, 0.6, 1), # gray highlight=(1, 1, 1, 1), # white action=(1, 0, 1, 1), # magenta ) + # plane color states plane_color = SelectorColorStates( idle=(0.1, 0.1, 0.1), # dark grey highlight=(0.2, 0.2, 0.2), # less dark grey @@ -115,10 +117,45 @@ def __init__( toolbar_visible, canvas_rect, ): + """ + Manages the plane mesh, resize handle point, and subplot title. + It also sets the viewport rects for the subplot rect and the rects of the docks. + + Note: This is a backend class not meant to be user-facing. + + Parameters + ---------- + viewport: pygfx.Viewport + Subplot viewport + + rect: tuple | np.ndarray + rect of this subplot + + extent: tuple | np.ndarray + extent of this subplot + + resizeable: bool + if the Frame is resizeable or not + + title: str + subplot title + + docks: dict[str, PlotArea] + subplot dock + + toolbar_visible: bool + toolbar visibility + + canvas_rect: tuple + figure canvas rect, the render area excluding any areas taken by imgui edge windows + + """ + self.viewport = viewport self.docks = docks self._toolbar_visible = toolbar_visible + # create rect manager to handle all the backend rect calculations if rect is not None: self._rect_manager = RectManager(*rect, canvas_rect) elif extent is not None: @@ -128,6 +165,7 @@ def __init__( wobjects = list() + # make title graphic if title is None: title_text = "" else: @@ -153,7 +191,7 @@ def __init__( pygfx.PointsMarkerMaterial( color=self.resize_handle_color.idle, marker="custom", - custom_sdf=sdf_wgsl_resize_handler, + custom_sdf=sdf_wgsl_resize_handle, size=12, size_space="screen", pick_write=True, @@ -161,6 +199,7 @@ def __init__( ) if not resizeable: + # set all color states to transparent if Frame isn't resizeable c = (0, 0, 0, 0) self._resize_handle.material.color = c self._resize_handle.material.edge_width = 0 @@ -168,12 +207,12 @@ def __init__( wobjects.append(self._resize_handle) - self._reset_plane() - self.reset_viewport() - self._world_object = pygfx.Group() self._world_object.add(*wobjects) + self._reset() + self.reset_viewport() + @property def rect_manager(self) -> RectManager: return self._rect_manager @@ -187,7 +226,7 @@ def extent(self) -> np.ndarray: @extent.setter def extent(self, extent): self._rect_manager.extent = extent - self._reset_plane() + self._reset() self.reset_viewport() @property @@ -198,13 +237,16 @@ def rect(self) -> np.ndarray[int]: @rect.setter def rect(self, rect: np.ndarray): self._rect_manager.rect = rect - self._reset_plane() + self._reset() self.reset_viewport() def reset_viewport(self): + """reset the viewport rect for the subplot and docks""" + # get rect of the render area x, y, w, h = self.get_render_rect() + # dock sizes s_left = self.docks["left"].size s_top = self.docks["top"].size s_right = self.docks["right"].size @@ -242,15 +284,16 @@ def get_render_rect(self) -> tuple[float, float, float, float]: Excludes area taken by the subplot title and toolbar. Also adds a small amount of spacing around the subplot. """ + # the rect of the entire Frame x, y, w, h = self.rect x += 1 # add 1 so a 1 pixel edge is visible w -= 2 # subtract 2, so we get a 1 pixel edge on both sides - y = ( - y + 4 + self._title_graphic.font_size + 4 - ) # add 4 pixels above and below title for better spacing + # add 4 pixels above and below title for better spacing + y = y + 4 + self._title_graphic.font_size + 4 + # spacing on the bottom if imgui toolbar is visible if self.toolbar_visible: toolbar_space = IMGUI_TOOLBAR_HEIGHT resize_handle_space = 0 @@ -272,7 +315,7 @@ def get_render_rect(self) -> tuple[float, float, float, float]: return x, y, w, h - def _reset_plane(self): + def _reset(self): """reset the plane mesh using the current rect state""" x0, x1, y0, y1 = self._rect_manager.extent @@ -280,9 +323,9 @@ def _reset_plane(self): self._plane.geometry.positions.data[masks.x0] = x0 self._plane.geometry.positions.data[masks.x1] = x1 - self._plane.geometry.positions.data[masks.y0] = ( - -y0 - ) # negative y because UnderlayCamera y is inverted + + # negative y because UnderlayCamera y is inverted + self._plane.geometry.positions.data[masks.y0] = -y0 self._plane.geometry.positions.data[masks.y1] = -y1 self._plane.geometry.positions.update_full() @@ -324,5 +367,5 @@ def resize_handle(self) -> pygfx.Points: def canvas_resized(self, canvas_rect): """called by layout is resized""" self._rect_manager.canvas_resized(canvas_rect) - self._reset_plane() + self._reset() self.reset_viewport() diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index 4ded32d24..d14e18785 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -20,7 +20,7 @@ class ImguiFigure(Figure): def __init__( self, - shape: list[tuple[int, int, int, int]] | tuple[int, int] = (1, 1), + shape: tuple[int, int] = (1, 1), rects=None, extents=None, cameras: ( @@ -172,7 +172,7 @@ def add_gui(self, gui: EdgeWindow): def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: """ - Fet rect for the portion of the canvas that the pygfx renderer draws to, + Get rect for the portion of the canvas that the pygfx renderer draws to, i.e. non-imgui, part of canvas Returns diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index 17c9feb82..6392d068d 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -2,6 +2,9 @@ class RectManager: + """ + Backend management of a rect. Allows converting between rects and extents, also works with fractional inputs. + """ def __init__(self, x: float, y: float, w: float, h: float, canvas_rect: tuple): # initialize rect state arrays # used to store internal state of the rect in both fractional screen space and absolute screen space @@ -42,7 +45,7 @@ def _set_from_fract(self, rect): if rect[1] + rect[3] > 1: raise ValueError("invalid fractional value: y + height > 1") - # assign values, don't just change the reference + # assign values to the arrays, don't just change the reference self._rect_frac[:] = rect self._rect_screen_space[:] = self._rect_frac * mult @@ -90,7 +93,7 @@ def rect(self, rect: np.ndarray | tuple): self._set(rect) def canvas_resized(self, canvas_rect: tuple): - # called by layout when canvas is resized + # called by Frame when canvas is resized self._canvas_rect[:] = canvas_rect # set new rect using existing rect_frac since this remains constant regardless of resize self._set(self._rect_frac) @@ -129,13 +132,13 @@ def extent(self) -> np.ndarray: @extent.setter def extent(self, extent): - """convert extent to rect""" rect = RectManager.extent_to_rect(extent, canvas_rect=self._canvas_rect) self._set(rect) @staticmethod def extent_to_rect(extent, canvas_rect): + """convert an extent to a rect""" RectManager.validate_extent(extent, canvas_rect) x0, x1, y0, y1 = extent @@ -208,7 +211,7 @@ def is_right_of(self, x1, dist: int = 1) -> bool: return self.x0 > x1 - dist def overlaps(self, extent: np.ndarray) -> bool: - """returns whether this subplot overlaps with the given extent""" + """returns whether this rect overlaps with the given extent""" x0, x1, y0, y1 = extent return not any( [ diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index d369b3658..cd78b8053 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -17,7 +17,7 @@ def __init__( self, parent: Union["Figure"], camera: Literal["2d", "3d"] | pygfx.PerspectiveCamera, - controller: pygfx.Controller, + controller: pygfx.Controller | str, canvas: BaseRenderCanvas | pygfx.Texture, rect: np.ndarray = None, extent: np.ndarray = None, @@ -26,8 +26,7 @@ def __init__( name: str = None, ): """ - General plot object is found within a ``Figure``. Each ``Figure`` instance will have [n rows, n columns] - of subplots. + Subplot class. .. important:: ``Subplot`` is not meant to be constructed directly, it only exists as part of a ``Figure`` @@ -37,9 +36,6 @@ def __init__( parent: 'Figure' | None parent Figure instance - position: (int, int), optional - corresponds to the [row, column] position of the subplot within a ``Figure`` - 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. @@ -103,10 +99,12 @@ def __init__( @property def axes(self) -> Axes: + """Axes object""" return self._axes @property def name(self) -> str: + """Subplot name""" return self._name @name.setter @@ -169,21 +167,12 @@ def frame(self) -> Frame: class Dock(PlotArea): - _valid_positions = ["right", "left", "top", "bottom"] - def __init__( self, parent: Subplot, - position: str, size: int, ): - if position not in self._valid_positions: - raise ValueError( - f"the `position` of an AnchoredViewport must be one of: {self._valid_positions}" - ) - self._size = size - self._position = position super().__init__( parent=parent, @@ -194,10 +183,6 @@ def __init__( renderer=parent.renderer, ) - @property - def position(self) -> str: - return self._position - @property def size(self) -> int: """Get or set the size of this dock""" From dbe436822b22f9a44b54383c219028fa2171f11d Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Mar 2025 02:11:24 -0400 Subject: [PATCH 51/82] docs api --- docs/source/api/layouts/figure.rst | 5 +++-- docs/source/api/layouts/imgui_figure.rst | 5 +++-- docs/source/api/layouts/subplot.rst | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/source/api/layouts/figure.rst b/docs/source/api/layouts/figure.rst index 3d6c745e9..b5cbbd2bb 100644 --- a/docs/source/api/layouts/figure.rst +++ b/docs/source/api/layouts/figure.rst @@ -23,11 +23,10 @@ Properties Figure.cameras Figure.canvas Figure.controllers - Figure.mode + Figure.layout Figure.names Figure.renderer Figure.shape - Figure.spacing Methods ~~~~~~~ @@ -35,6 +34,7 @@ Methods :toctree: Figure_api Figure.add_animations + Figure.add_subplot Figure.clear Figure.close Figure.export @@ -42,5 +42,6 @@ Methods Figure.get_pygfx_render_area Figure.open_popup Figure.remove_animation + Figure.remove_subplot Figure.show diff --git a/docs/source/api/layouts/imgui_figure.rst b/docs/source/api/layouts/imgui_figure.rst index 6d6bb2dd4..a338afe96 100644 --- a/docs/source/api/layouts/imgui_figure.rst +++ b/docs/source/api/layouts/imgui_figure.rst @@ -25,11 +25,10 @@ Properties ImguiFigure.controllers ImguiFigure.guis ImguiFigure.imgui_renderer - ImguiFigure.mode + ImguiFigure.layout ImguiFigure.names ImguiFigure.renderer ImguiFigure.shape - ImguiFigure.spacing Methods ~~~~~~~ @@ -38,6 +37,7 @@ Methods ImguiFigure.add_animations ImguiFigure.add_gui + ImguiFigure.add_subplot ImguiFigure.clear ImguiFigure.close ImguiFigure.export @@ -46,5 +46,6 @@ Methods ImguiFigure.open_popup ImguiFigure.register_popup ImguiFigure.remove_animation + ImguiFigure.remove_subplot ImguiFigure.show diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index 1cf9be31c..e1c55514d 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -26,6 +26,7 @@ Properties Subplot.canvas Subplot.controller Subplot.docks + Subplot.frame Subplot.graphics Subplot.legends Subplot.name @@ -34,6 +35,7 @@ Properties Subplot.renderer Subplot.scene Subplot.selectors + Subplot.title Subplot.toolbar Subplot.viewport @@ -53,7 +55,6 @@ Methods Subplot.auto_scale Subplot.center_graphic Subplot.center_scene - Subplot.center_title Subplot.clear Subplot.delete_graphic Subplot.get_figure @@ -61,5 +62,4 @@ Methods Subplot.map_screen_to_world Subplot.remove_animation Subplot.remove_graphic - Subplot.set_title From f252e7027b7779935dd60cb1f72eb02791c6f758 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Mar 2025 02:13:07 -0400 Subject: [PATCH 52/82] black --- fastplotlib/layouts/_rect.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index 6392d068d..846b29db3 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -5,6 +5,7 @@ class RectManager: """ Backend management of a rect. Allows converting between rects and extents, also works with fractional inputs. """ + def __init__(self, x: float, y: float, w: float, h: float, canvas_rect: tuple): # initialize rect state arrays # used to store internal state of the rect in both fractional screen space and absolute screen space From 69d6b65302b9967f42968efc900ee2c5cea5a7df Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Mar 2025 02:15:53 -0400 Subject: [PATCH 53/82] fix --- fastplotlib/layouts/_subplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index cd78b8053..628ed46f0 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -78,7 +78,7 @@ def __init__( ) for pos in ["left", "top", "right", "bottom"]: - dv = Dock(self, pos, size=0) + dv = Dock(self, size=0) dv.name = pos self.docks[pos] = dv self.children.append(dv) From 4a053fe039422c48dd1fd7fe5015d120992ec5b7 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Mar 2025 02:38:16 -0400 Subject: [PATCH 54/82] cleanup --- fastplotlib/ui/right_click_menus/_standard_menu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fastplotlib/ui/right_click_menus/_standard_menu.py b/fastplotlib/ui/right_click_menus/_standard_menu.py index 0a7fbd619..1937df858 100644 --- a/fastplotlib/ui/right_click_menus/_standard_menu.py +++ b/fastplotlib/ui/right_click_menus/_standard_menu.py @@ -57,7 +57,6 @@ def update(self): if self._last_right_click_pos == imgui.get_mouse_pos(): if self.get_subplot() is not False: # must explicitly check for False # open only if right click was inside a subplot - print("opening right click menu") imgui.open_popup(f"right-click-menu") # TODO: call this just once when going from open -> closed state From 526e94ee9114a8c87b74f0bcd9b2e902d586051f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Mar 2025 03:02:47 -0400 Subject: [PATCH 55/82] add README.rst for flex layouts examples dir --- examples/flex_layouts/README.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 examples/flex_layouts/README.rst diff --git a/examples/flex_layouts/README.rst b/examples/flex_layouts/README.rst new file mode 100644 index 000000000..892c1714f --- /dev/null +++ b/examples/flex_layouts/README.rst @@ -0,0 +1,2 @@ +FlexLayout Examples +=================== From c7656d4bd739643ad660c0109f462408d01d3ea1 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Mar 2025 03:10:06 -0400 Subject: [PATCH 56/82] add flex layouts to test utils list --- examples/tests/testutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index f72a87123..b4c62bd1e 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -24,6 +24,7 @@ "line/*.py", "line_collection/*.py", "gridplot/*.py", + "flex_layouts/*.py" "misc/*.py", "selection_tools/*.py", "guis/*.py", From e0e163baaf4172d804116cf3abc9b07d0e1c28aa Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Mar 2025 03:14:42 -0400 Subject: [PATCH 57/82] add spinning spiral scatter example --- examples/scatter/spinning_spiral.py | 62 +++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 examples/scatter/spinning_spiral.py diff --git a/examples/scatter/spinning_spiral.py b/examples/scatter/spinning_spiral.py new file mode 100644 index 000000000..c032fc1c8 --- /dev/null +++ b/examples/scatter/spinning_spiral.py @@ -0,0 +1,62 @@ +""" +Spinning spiral scatter +======================= + +Example of a spinning spiral scatter +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate 10s' + +import numpy as np +import fastplotlib as fpl + +# number of points +n = 100_000 + +# create data in the shape of a spiral +phi = np.linspace(0, 30, n) + +xs = phi * np.cos(phi) + np.random.normal(scale=1.5, size=n) +ys = np.random.normal(scale=1, size=n) +zs = phi * np.sin(phi) + np.random.normal(scale=1.5, size=n) + +data = np.column_stack([xs, ys, zs]) + +figure = fpl.Figure(cameras="3d", size=(700, 560)) + +spiral = figure[0, 0].add_scatter(data, cmap="viridis_r", alpha=0.8) + + +def update(): + # rotate around y axis + spiral.rotate(0.005, axis="y") + # add small jitter + spiral.data[:] += np.random.normal(scale=0.01, size=n * 3).reshape((n, 3)) + + +figure.add_animations(update) +figure.show() + +# pre-saved camera state +camera_state = { + 'position': np.array([-0.13046005, 20.09142224, 29.03347696]), + 'rotation': np.array([-0.44485092, 0.05335406, 0.11586037, 0.88647469]), + 'scale': np.array([1., 1., 1.]), + 'reference_up': np.array([0., 1., 0.]), + 'fov': 50.0, + 'width': 62.725074768066406, + 'height': 8.856056690216064, + 'zoom': 0.75, + 'maintain_aspect': True, + 'depth_range': None +} +figure[0, 0].camera.set_state(camera_state) +figure[0, 0].axes.visible = False + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() From 354d1095081c24b19ff9cac7ccf15665548da5c8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Mar 2025 03:16:09 -0400 Subject: [PATCH 58/82] modify docs conf --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 76298d4ff..5899c68e2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -59,6 +59,7 @@ "../../examples/heatmap", "../../examples/image_widget", "../../examples/gridplot", + "../../examples/flex_layouts", "../../examples/line", "../../examples/line_collection", "../../examples/scatter", From ac8341c9941621ce44c001fd519fb1ffccfbfd1f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Mar 2025 03:48:49 -0400 Subject: [PATCH 59/82] forgot a comma --- examples/tests/testutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index b4c62bd1e..fb09ebd48 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -24,7 +24,7 @@ "line/*.py", "line_collection/*.py", "gridplot/*.py", - "flex_layouts/*.py" + "flex_layouts/*.py", "misc/*.py", "selection_tools/*.py", "guis/*.py", From b8ed276ea90731a7c8ead9afd9dd00b2d760fc8c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 10 Mar 2025 04:27:44 -0400 Subject: [PATCH 60/82] add rect extent ground truths --- examples/screenshots/extent_frac_layout.png | 3 +++ examples/screenshots/extent_layout.png | 3 +++ examples/screenshots/no-imgui-extent_frac_layout.png | 3 +++ examples/screenshots/no-imgui-extent_layout.png | 3 +++ examples/screenshots/no-imgui-rect_frac_layout.png | 3 +++ examples/screenshots/no-imgui-rect_layout.png | 3 +++ examples/screenshots/rect_frac_layout.png | 3 +++ examples/screenshots/rect_layout.png | 3 +++ 8 files changed, 24 insertions(+) create mode 100644 examples/screenshots/extent_frac_layout.png create mode 100644 examples/screenshots/extent_layout.png create mode 100644 examples/screenshots/no-imgui-extent_frac_layout.png create mode 100644 examples/screenshots/no-imgui-extent_layout.png create mode 100644 examples/screenshots/no-imgui-rect_frac_layout.png create mode 100644 examples/screenshots/no-imgui-rect_layout.png create mode 100644 examples/screenshots/rect_frac_layout.png create mode 100644 examples/screenshots/rect_layout.png diff --git a/examples/screenshots/extent_frac_layout.png b/examples/screenshots/extent_frac_layout.png new file mode 100644 index 000000000..7fe6d3d37 --- /dev/null +++ b/examples/screenshots/extent_frac_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5991b755432318310cfc2b4826bd9639cc234883aa06f1895817f710714cb58f +size 156297 diff --git a/examples/screenshots/extent_layout.png b/examples/screenshots/extent_layout.png new file mode 100644 index 000000000..7677dc4da --- /dev/null +++ b/examples/screenshots/extent_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c7b55c3c510169fe21422f11dfd27677284c74e1c2e4c91c7c34839afa0bdee +size 148166 diff --git a/examples/screenshots/no-imgui-extent_frac_layout.png b/examples/screenshots/no-imgui-extent_frac_layout.png new file mode 100644 index 000000000..4dc3b2aa6 --- /dev/null +++ b/examples/screenshots/no-imgui-extent_frac_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5923e8b9f687f97d488b282b35f16234898ed1038b0737b7b57fb9cbd72ebf34 +size 157321 diff --git a/examples/screenshots/no-imgui-extent_layout.png b/examples/screenshots/no-imgui-extent_layout.png new file mode 100644 index 000000000..928e5c391 --- /dev/null +++ b/examples/screenshots/no-imgui-extent_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a5be8a506039eb509a2bac530eb723847fb2af21de17166dff08e2bf6d5d0d6 +size 149532 diff --git a/examples/screenshots/no-imgui-rect_frac_layout.png b/examples/screenshots/no-imgui-rect_frac_layout.png new file mode 100644 index 000000000..4dc3b2aa6 --- /dev/null +++ b/examples/screenshots/no-imgui-rect_frac_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5923e8b9f687f97d488b282b35f16234898ed1038b0737b7b57fb9cbd72ebf34 +size 157321 diff --git a/examples/screenshots/no-imgui-rect_layout.png b/examples/screenshots/no-imgui-rect_layout.png new file mode 100644 index 000000000..928e5c391 --- /dev/null +++ b/examples/screenshots/no-imgui-rect_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a5be8a506039eb509a2bac530eb723847fb2af21de17166dff08e2bf6d5d0d6 +size 149532 diff --git a/examples/screenshots/rect_frac_layout.png b/examples/screenshots/rect_frac_layout.png new file mode 100644 index 000000000..7fe6d3d37 --- /dev/null +++ b/examples/screenshots/rect_frac_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5991b755432318310cfc2b4826bd9639cc234883aa06f1895817f710714cb58f +size 156297 diff --git a/examples/screenshots/rect_layout.png b/examples/screenshots/rect_layout.png new file mode 100644 index 000000000..7677dc4da --- /dev/null +++ b/examples/screenshots/rect_layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c7b55c3c510169fe21422f11dfd27677284c74e1c2e4c91c7c34839afa0bdee +size 148166 From bc48fe3eceae18bcd5d3cc74b28189c4a1c37526 Mon Sep 17 00:00:00 2001 From: clewis7 Date: Mon, 10 Mar 2025 16:46:05 -0400 Subject: [PATCH 61/82] fix text --- fastplotlib/graphics/text.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index fcee6129b..68caa471c 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -79,18 +79,16 @@ def __init__( self._outline_thickness = TextOutlineThickness(outline_thickness) world_object = pygfx.Text( - pygfx.TextGeometry( text=self.text, font_size=self.font_size, screen_space=screen_space, anchor=anchor, - ), - pygfx.TextMaterial( - color=self.face_color, - outline_color=self.outline_color, - outline_thickness=self.outline_thickness, - pick_write=True, - ), + material=pygfx.TextMaterial( + color=self.face_color, + outline_color=self.outline_color, + outline_thickness=self.outline_thickness, + pick_write=True, + ), ) self._set_world_object(world_object) From e5f28e1b88e58f8d7d2a7cbae37500271a418206 Mon Sep 17 00:00:00 2001 From: clewis7 Date: Mon, 10 Mar 2025 17:03:38 -0400 Subject: [PATCH 62/82] fix text features --- fastplotlib/graphics/_features/_text.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastplotlib/graphics/_features/_text.py b/fastplotlib/graphics/_features/_text.py index 90af7c719..a95fe256c 100644 --- a/fastplotlib/graphics/_features/_text.py +++ b/fastplotlib/graphics/_features/_text.py @@ -16,7 +16,7 @@ def value(self) -> str: @block_reentrance def set_value(self, graphic, value: str): - graphic.world_object.geometry.set_text(value) + graphic.world_object.set_text(value) self._value = value event = FeatureEvent(type="text", info={"value": value}) @@ -34,8 +34,8 @@ def value(self) -> float | int: @block_reentrance def set_value(self, graphic, value: float | int): - graphic.world_object.geometry.font_size = value - self._value = graphic.world_object.geometry.font_size + graphic.world_object.font_size = value + self._value = graphic.world_object.font_size event = FeatureEvent(type="font_size", info={"value": value}) self._call_event_handlers(event) From e76a77e875002a5db0a3ad8da0dcb919621381d6 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Mar 2025 10:13:24 -0400 Subject: [PATCH 63/82] types --- fastplotlib/layouts/_engine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index eaf76aea7..86d6db225 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -113,7 +113,7 @@ class FlexLayout(BaseLayout): def __init__( self, renderer, - subplots: list[Subplot], + subplots: np.ndarray[Subplot], canvas_rect: tuple, moveable=True, resizeable=True, @@ -307,7 +307,7 @@ class GridLayout(FlexLayout): def __init__( self, renderer, - subplots: list[Subplot], + subplots: np.ndarray[Subplot], canvas_rect: tuple[float, float, float, float], shape: tuple[int, int], ): From a26e24a68fb8f3fbcd49e61a183d988724141c09 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Mar 2025 10:29:47 -0400 Subject: [PATCH 64/82] comments, docstrings --- examples/flex_layouts/extent_frac_layout.py | 4 +-- examples/flex_layouts/extent_layout.py | 4 +-- examples/flex_layouts/rect_frac_layout.py | 4 +-- examples/flex_layouts/rect_layout.py | 4 +-- fastplotlib/layouts/_engine.py | 40 +++++++++++++++++---- fastplotlib/layouts/_figure.py | 2 +- 6 files changed, 43 insertions(+), 15 deletions(-) diff --git a/examples/flex_layouts/extent_frac_layout.py b/examples/flex_layouts/extent_frac_layout.py index 562c5814f..0c5293e09 100644 --- a/examples/flex_layouts/extent_frac_layout.py +++ b/examples/flex_layouts/extent_frac_layout.py @@ -2,8 +2,8 @@ Fractional Extent Layout ======================== -Create subplots using extents given as fractions of the canvas. This example plots two images and their histograms in -separate subplots +Create subplots using extents given as fractions of the canvas. +This example plots two images and their histograms in separate subplots """ diff --git a/examples/flex_layouts/extent_layout.py b/examples/flex_layouts/extent_layout.py index 022ab9d5e..535739ed1 100644 --- a/examples/flex_layouts/extent_layout.py +++ b/examples/flex_layouts/extent_layout.py @@ -2,8 +2,8 @@ Extent Layout ============= -Create subplots using given extents in absolute pixels. This example plots two images and their histograms in -separate subplots +Create subplots using given extents in absolute pixels. +This example plots two images and their histograms in separate subplots """ diff --git a/examples/flex_layouts/rect_frac_layout.py b/examples/flex_layouts/rect_frac_layout.py index bb60453f1..072fa1107 100644 --- a/examples/flex_layouts/rect_frac_layout.py +++ b/examples/flex_layouts/rect_frac_layout.py @@ -2,8 +2,8 @@ Rect Fractional Layout ====================== -Create subplots using rects given as fractions of the canvas. This example plots two images and their histograms in -separate subplots +Create subplots using rects given as fractions of the canvas. +This example plots two images and their histograms in separate subplots """ diff --git a/examples/flex_layouts/rect_layout.py b/examples/flex_layouts/rect_layout.py index 4b8b9d607..ec81ac157 100644 --- a/examples/flex_layouts/rect_layout.py +++ b/examples/flex_layouts/rect_layout.py @@ -2,8 +2,8 @@ Rect Layout =========== -Create subplots using given rects in absolute pixels. This example plots two images and their histograms in -separate subplots +Create subplots using given rects in absolute pixels. +This example plots two images and their histograms in separate subplots """ diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 86d6db225..dcae3e645 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -11,8 +11,9 @@ class UnderlayCamera(pygfx.Camera): """ Same as pygfx.ScreenCoordsCamera but y-axis is inverted. - So top right is (0, 0). This is easier to manage because we + So top left corner is (0, 0). This is easier to manage because we often resize using the bottom right corner. + """ def _update_projection_matrix(self): @@ -34,6 +35,9 @@ def __init__( moveable: bool, resizeable: bool, ): + """ + Base layout engine, subclass to create a useable layout engine. + """ self._renderer = renderer self._subplots: np.ndarray[Subplot] = subplots.ravel() self._canvas_rect = canvas_rect @@ -42,8 +46,11 @@ def __init__( [np.nan, np.nan] ) + # the current user action, move or resize self._active_action: str | None = None + # subplot that is currently in action, i.e. currently being moved or resized self._active_subplot: Subplot | None = None + # subplot that is in focus, i.e. being hovered by the pointer self._subplot_focus: Subplot | None = None for subplot in self._subplots: @@ -76,6 +83,16 @@ def _inside_render_rect(self, subplot: Subplot, pos: tuple[int, int]) -> bool: return False def canvas_resized(self, canvas_rect: tuple): + """ + called by figure when canvas is resized + + Parameters + ---------- + canvas_rect: (x, y, w, h) + the rect that pygfx can render to, excludes any areas used by imgui. + + """ + self._canvas_rect = canvas_rect for subplot in self._subplots: subplot.frame.canvas_resized(canvas_rect) @@ -118,16 +135,21 @@ def __init__( moveable=True, resizeable=True, ): + """ + Flexible layout engine that allows freely moving and resizing subplots. + Subplots are not allowed to overlap. + + We use a screenspace camera to perform an underlay render pass to draw the + subplot frames, there is no depth rendering so we do not allow overlaps. + + """ + super().__init__(renderer, subplots, canvas_rect, moveable, resizeable) self._last_pointer_pos: np.ndarray[np.float64, np.float64] = np.array( [np.nan, np.nan] ) - self._active_action: str | None = None - self._active_subplot: Subplot | None = None - self._subplot_focus: Subplot | None = None - for subplot in self._subplots: if moveable: # start a move action @@ -216,7 +238,7 @@ def _action_start(self, subplot: Subplot, action: str, ev): if self._inside_render_rect(subplot, pos=(ev.x, ev.y)): return - if ev.button == 1: + if ev.button == 1: # left mouse button self._active_action = action if action == "resize": subplot.frame.resize_handle.material.color = ( @@ -311,6 +333,12 @@ def __init__( canvas_rect: tuple[float, float, float, float], shape: tuple[int, int], ): + """ + Grid layout engine that auto-sets Frame and Subplot rects such that they maintain + a fixed grid layout. Does not allow freely moving or resizing subplots. + + """ + super().__init__( renderer, subplots, canvas_rect, moveable=False, resizeable=False ) diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index a234ff186..225a1d699 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -737,7 +737,7 @@ def _fpl_reset_layout(self, *ev): def get_pygfx_render_area(self, *args) -> tuple[float, float, float, float]: """ - Fet rect for the portion of the canvas that the pygfx renderer draws to, + Get rect for the portion of the canvas that the pygfx renderer draws to, i.e. non-imgui, part of canvas Returns From 7aaeb0f44ae005671db2d2df7c25355d7bdc0077 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 11 Mar 2025 10:58:17 -0400 Subject: [PATCH 65/82] update w.r.t. text changes --- fastplotlib/graphics/text.py | 5 +++++ tests/test_text_graphic.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 68caa471c..4a8a33543 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -95,6 +95,11 @@ def __init__( self.offset = offset + @property + def world_object(self) -> pygfx.Text: + """Text world object""" + return super(TextGraphic, self).world_object + @property def text(self) -> str: """the text displayed""" diff --git a/tests/test_text_graphic.py b/tests/test_text_graphic.py index a13dfe690..deb25ca6b 100644 --- a/tests/test_text_graphic.py +++ b/tests/test_text_graphic.py @@ -25,7 +25,7 @@ def test_create_graphic(): assert text.font_size == 14 assert isinstance(text._font_size, FontSize) - assert text.world_object.geometry.font_size == 14 + assert text.world_object.font_size == 14 assert text.face_color == pygfx.Color("w") assert isinstance(text._face_color, TextFaceColor) @@ -82,7 +82,7 @@ def test_text_changes_events(): text.font_size = 10.0 assert text.font_size == 10.0 - assert text.world_object.geometry.font_size == 10 + assert text.world_object.font_size == 10 check_event(text, "font_size", 10) text.face_color = "r" From 8e0e27af6d12386758078908f634631517fd6610 Mon Sep 17 00:00:00 2001 From: clewis7 Date: Thu, 13 Mar 2025 13:04:14 -0400 Subject: [PATCH 66/82] small typos # Conflicts: # fastplotlib/layouts/_engine.py --- fastplotlib/layouts/_figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 225a1d699..7e822504d 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -132,7 +132,7 @@ def __init__( else: if not all(isinstance(v, (int, np.integer)) for v in shape): raise TypeError( - "shape argument must be a list of bounding boxes or a tuple[n_rows, n_cols]" + "shape argument must be a tuple[n_rows, n_cols]" ) n_subplots = shape[0] * shape[1] layout_mode = "grid" From 1264ec6592d61f5dcf04b2af90c63892a73cdf5b Mon Sep 17 00:00:00 2001 From: clewis7 Date: Thu, 13 Mar 2025 13:07:01 -0400 Subject: [PATCH 67/82] small typos --- fastplotlib/layouts/_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_frame.py b/fastplotlib/layouts/_frame.py index a6b4f9ef9..dd145acbf 100644 --- a/fastplotlib/layouts/_frame.py +++ b/fastplotlib/layouts/_frame.py @@ -49,7 +49,7 @@ class MeshMasks: - """Used set the x1, x1, y0, y1 positions of the plane mesh""" + """Used set the x0, x1, y0, y1 positions of the plane mesh""" x0 = np.array( [ From 61ac3fffcb4a145ee9d480e098eb9bcd10fb47a3 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 13 Mar 2025 18:42:00 -0400 Subject: [PATCH 68/82] Update fastplotlib/layouts/_engine.py Co-authored-by: Almar Klein --- fastplotlib/layouts/_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index dcae3e645..ad3e94c33 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -36,7 +36,7 @@ def __init__( resizeable: bool, ): """ - Base layout engine, subclass to create a useable layout engine. + Base layout engine, subclass to create a usable layout engine. """ self._renderer = renderer self._subplots: np.ndarray[Subplot] = subplots.ravel() From 8b9a466b0f8faf9736298b2a8aea7ad15d86019b Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 13 Mar 2025 21:39:13 -0400 Subject: [PATCH 69/82] rename FlexLayout -> WindowLayout --- examples/flex_layouts/README.rst | 2 -- examples/window_layouts/README.rst | 2 ++ .../extent_frac_layout.py | 0 .../extent_layout.py | 0 .../rect_frac_layout.py | 0 .../rect_layout.py | 0 fastplotlib/layouts/_engine.py | 4 ++-- fastplotlib/layouts/_figure.py | 16 +++++++++++----- fastplotlib/utils/{_types.py => types.py} | 0 9 files changed, 15 insertions(+), 9 deletions(-) delete mode 100644 examples/flex_layouts/README.rst create mode 100644 examples/window_layouts/README.rst rename examples/{flex_layouts => window_layouts}/extent_frac_layout.py (100%) rename examples/{flex_layouts => window_layouts}/extent_layout.py (100%) rename examples/{flex_layouts => window_layouts}/rect_frac_layout.py (100%) rename examples/{flex_layouts => window_layouts}/rect_layout.py (100%) rename fastplotlib/utils/{_types.py => types.py} (100%) diff --git a/examples/flex_layouts/README.rst b/examples/flex_layouts/README.rst deleted file mode 100644 index 892c1714f..000000000 --- a/examples/flex_layouts/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -FlexLayout Examples -=================== diff --git a/examples/window_layouts/README.rst b/examples/window_layouts/README.rst new file mode 100644 index 000000000..320a72b72 --- /dev/null +++ b/examples/window_layouts/README.rst @@ -0,0 +1,2 @@ +WindowLayout Examples +=================== diff --git a/examples/flex_layouts/extent_frac_layout.py b/examples/window_layouts/extent_frac_layout.py similarity index 100% rename from examples/flex_layouts/extent_frac_layout.py rename to examples/window_layouts/extent_frac_layout.py diff --git a/examples/flex_layouts/extent_layout.py b/examples/window_layouts/extent_layout.py similarity index 100% rename from examples/flex_layouts/extent_layout.py rename to examples/window_layouts/extent_layout.py diff --git a/examples/flex_layouts/rect_frac_layout.py b/examples/window_layouts/rect_frac_layout.py similarity index 100% rename from examples/flex_layouts/rect_frac_layout.py rename to examples/window_layouts/rect_frac_layout.py diff --git a/examples/flex_layouts/rect_layout.py b/examples/window_layouts/rect_layout.py similarity index 100% rename from examples/flex_layouts/rect_layout.py rename to examples/window_layouts/rect_layout.py diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index ad3e94c33..684417e21 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -126,7 +126,7 @@ def __len__(self): return len(self._subplots) -class FlexLayout(BaseLayout): +class WindowLayout(BaseLayout): def __init__( self, renderer, @@ -325,7 +325,7 @@ def set_extent(self, subplot: Subplot, extent: tuple | list | np.ndarray): ) -class GridLayout(FlexLayout): +class GridLayout(WindowLayout): def __init__( self, renderer, diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 7e822504d..11e937ae8 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -19,7 +19,7 @@ ) from ._utils import controller_types as valid_controller_types from ._subplot import Subplot -from ._engine import BaseLayout, GridLayout, FlexLayout, UnderlayCamera +from ._engine import GridLayout, WindowLayout, UnderlayCamera from .. import ImageGraphic @@ -65,9 +65,11 @@ def __init__( extents: list of tuples or arrays list of extents (xmin, xmax, ymin, ymax) that define the subplots. - extents can be defined in absolute pixels or as a fraction of the canvas + extents can be defined in absolute pixels or as a fraction of the canvas. + If both ``rects`` and ``extents`` are provided, then ``rects`` takes precedence over ``extents``, i.e. + ``extents`` is ignored when ``rects`` are also provided. - cameras: "2d", "3", list of "2d" | "3d", Iterable of camera instances, or Iterable of "2d" | "3d", optional + cameras: "2d", "3d", list of "2d" | "3d", Iterable of camera instances, or Iterable of "2d" | "3d", optional | if str, one of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots | Iterable/list/array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot | Iterable/list/array of pygfx.PerspectiveCamera instances @@ -386,7 +388,7 @@ def __init__( ) elif layout_mode == "rect" or layout_mode == "extent": - self._layout = FlexLayout( + self._layout = WindowLayout( self.renderer, subplots=self._subplots, canvas_rect=self.get_pygfx_render_area(), @@ -417,7 +419,7 @@ def shape(self) -> list[tuple[int, int, int, int]] | tuple[int, int]: return self.layout.shape @property - def layout(self) -> FlexLayout | GridLayout: + def layout(self) -> WindowLayout | GridLayout: """ Layout engine """ @@ -480,6 +482,10 @@ def _render(self, draw=True): # call post-render animate functions self._call_animate_functions(self._animate_funcs_post) + if draw: + # needs to be here else events don't get processed + self.canvas.request_draw() + def _start_render(self): """start render cycle""" self.canvas.request_draw(self._render) diff --git a/fastplotlib/utils/_types.py b/fastplotlib/utils/types.py similarity index 100% rename from fastplotlib/utils/_types.py rename to fastplotlib/utils/types.py From 996bbe7770394a81a1c8b1fabae8600fc6af125a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 13 Mar 2025 21:39:44 -0400 Subject: [PATCH 70/82] better check for imgui --- fastplotlib/layouts/_subplot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 628ed46f0..73f669fe5 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -6,7 +6,7 @@ from rendercanvas import BaseRenderCanvas from ..graphics import TextGraphic -from ._utils import create_camera, create_controller, IMGUI +from ._utils import create_camera, create_controller from ._plot_area import PlotArea from ._frame import Frame from ..graphics._axes import Axes @@ -62,7 +62,7 @@ def __init__( self._docks = dict() - if IMGUI: + if "Imgui" in parent.__class__.__name__: toolbar_visible = True else: toolbar_visible = False From 0bc960000db286b232074fbdefa67d3e826de406 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 13 Mar 2025 21:40:07 -0400 Subject: [PATCH 71/82] imports --- fastplotlib/layouts/_frame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastplotlib/layouts/_frame.py b/fastplotlib/layouts/_frame.py index dd145acbf..cd2a1cbc2 100644 --- a/fastplotlib/layouts/_frame.py +++ b/fastplotlib/layouts/_frame.py @@ -2,8 +2,8 @@ import pygfx from ._rect import RectManager -from ._utils import IMGUI, IMGUI_TOOLBAR_HEIGHT -from ..utils._types import SelectorColorStates +from ._utils import IMGUI_TOOLBAR_HEIGHT +from ..utils.types import SelectorColorStates from ..graphics import TextGraphic From 0efd2b2c50b9d75d3ad8be7fadd4eba0d9b7554d Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 13 Mar 2025 21:40:41 -0400 Subject: [PATCH 72/82] comments --- fastplotlib/layouts/_imgui_figure.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index d14e18785..f6d3da20f 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -113,6 +113,8 @@ def _render(self, draw=False): super()._render(draw) self.imgui_renderer.render() + + # needs to be here else events don't get processed self.canvas.request_draw() def _draw_imgui(self) -> imgui.ImDrawData: From 302deb5297c749ccd4ae9567fbead80c2c8042e8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 13 Mar 2025 21:41:07 -0400 Subject: [PATCH 73/82] example tests files moved --- docs/source/conf.py | 2 +- examples/tests/testutils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 5899c68e2..865c462a6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -59,7 +59,7 @@ "../../examples/heatmap", "../../examples/image_widget", "../../examples/gridplot", - "../../examples/flex_layouts", + "../../examples/window_layouts", "../../examples/line", "../../examples/line_collection", "../../examples/scatter", diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index fb09ebd48..d6fce52fe 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -24,7 +24,7 @@ "line/*.py", "line_collection/*.py", "gridplot/*.py", - "flex_layouts/*.py", + "window_layouts/*.py", "misc/*.py", "selection_tools/*.py", "guis/*.py", From db14148e118acefb79ae458df0fa2eb5f8362530 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 13 Mar 2025 22:42:32 -0400 Subject: [PATCH 74/82] smaller canvas initial size for abs pixels until rendercanvs fix for glfw --- examples/window_layouts/extent_layout.py | 10 +++++----- examples/window_layouts/rect_layout.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/window_layouts/extent_layout.py b/examples/window_layouts/extent_layout.py index 535739ed1..e6facaaa2 100644 --- a/examples/window_layouts/extent_layout.py +++ b/examples/window_layouts/extent_layout.py @@ -26,15 +26,15 @@ centers_2 = edges_2[:-1] + np.diff(edges_2) / 2 # figure size in pixels -size = (700, 560) +size = (640, 480) # extent is (xmin, xmax, ymin, ymax) # here it is defined in absolute pixels extents = [ - (0, 200, 0, 280), # for image1 - (0, 200, 280, 560), # for image2 - (200, 700, 0, 280), # for image1 histogram - (200, 700, 280, 560), # for image2 histogram + (0, 200, 0, 240), # for image1 + (0, 200, 240, 480), # for image2 + (200, 640, 0, 240), # for image1 histogram + (200, 640, 240, 480), # for image2 histogram ] # create a figure using the rects and size diff --git a/examples/window_layouts/rect_layout.py b/examples/window_layouts/rect_layout.py index ec81ac157..962b8a4f1 100644 --- a/examples/window_layouts/rect_layout.py +++ b/examples/window_layouts/rect_layout.py @@ -26,15 +26,15 @@ centers_2 = edges_2[:-1] + np.diff(edges_2) / 2 # figure size in pixels -size = (700, 560) +size = (640, 480) # a rect is (x, y, width, height) # here it is defined in absolute pixels rects = [ - (0, 0, 200, 280), # for image1 - (0, 280, 200, 280), # for image2 - (200, 0, 500, 280), # for image1 histogram - (200, 280, 500, 280), # for image2 histogram + (0, 0, 200, 240), # for image1 + (0, 240, 200, 240), # for image2 + (200, 0, 440, 240), # for image1 histogram + (200, 240, 440, 240), # for image2 histogram ] # create a figure using the rects and size From 0976aa4684eaf131ed23598f21d33040b569dddf Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 13 Mar 2025 23:08:29 -0400 Subject: [PATCH 75/82] better error messages --- fastplotlib/layouts/_rect.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index 846b29db3..20b61ccff 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -24,7 +24,7 @@ def _set(self, rect): rect = np.asarray(rect) for val, name in zip(rect, ["x-position", "y-position", "width", "height"]): if val < 0: - raise ValueError(f"Invalid rect value < 0: {rect}") + raise ValueError(f"Invalid rect value < 0: {rect}\n All values must be non-negative.") if (rect[2:] <= 1).all(): # fractional bbox self._set_from_fract(rect) @@ -42,9 +42,9 @@ def _set_from_fract(self, rect): # check that widths, heights are valid: if rect[0] + rect[2] > 1: - raise ValueError("invalid fractional value: x + width > 1") + raise ValueError(f"invalid fractional rect: {rect}\n x + width > 1: {rect[0]} + {rect[2]} > 1") if rect[1] + rect[3] > 1: - raise ValueError("invalid fractional value: y + height > 1") + raise ValueError(f"invalid fractional rect: {rect}\n y + height > 1: {rect[1]} + {rect[3]} > 1") # assign values to the arrays, don't just change the reference self._rect_frac[:] = rect @@ -57,9 +57,9 @@ def _set_from_screen_space(self, rect): # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 # check that widths, heights are valid if rect[0] + rect[2] > cw: - raise ValueError(f"invalid value: x + width > 1: {rect}") + raise ValueError(f"invalid rect: {rect}\n x + width > canvas width: {rect[0]} + {rect[2]} > {cw}") if rect[1] + rect[3] > ch: - raise ValueError(f"invalid value: y + height > 1: {rect}") + raise ValueError(f"invalid rect: {rect}\n y + height > canvas height: {rect[1]} + {rect[3]} >{ch}") self._rect_frac[:] = rect / mult self._rect_screen_space[:] = rect From 6b318eeaacab343569760979e9e712681eb6ab09 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 13 Mar 2025 23:29:34 -0400 Subject: [PATCH 76/82] update screenshots --- examples/screenshots/extent_layout.png | 4 ++-- examples/screenshots/rect_layout.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/screenshots/extent_layout.png b/examples/screenshots/extent_layout.png index 7677dc4da..dec391ac2 100644 --- a/examples/screenshots/extent_layout.png +++ b/examples/screenshots/extent_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c7b55c3c510169fe21422f11dfd27677284c74e1c2e4c91c7c34839afa0bdee -size 148166 +oid sha256:0cf23f845932023789e0823a105910e9f701d0f03c04e3c18488f0da62420921 +size 123409 diff --git a/examples/screenshots/rect_layout.png b/examples/screenshots/rect_layout.png index 7677dc4da..dec391ac2 100644 --- a/examples/screenshots/rect_layout.png +++ b/examples/screenshots/rect_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c7b55c3c510169fe21422f11dfd27677284c74e1c2e4c91c7c34839afa0bdee -size 148166 +oid sha256:0cf23f845932023789e0823a105910e9f701d0f03c04e3c18488f0da62420921 +size 123409 From 5123f864cc205bc5417c2d7e00684712abedeb43 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 13 Mar 2025 23:31:51 -0400 Subject: [PATCH 77/82] update screenshots --- examples/screenshots/no-imgui-extent_layout.png | 4 ++-- examples/screenshots/no-imgui-rect_layout.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/screenshots/no-imgui-extent_layout.png b/examples/screenshots/no-imgui-extent_layout.png index 928e5c391..16d1ff446 100644 --- a/examples/screenshots/no-imgui-extent_layout.png +++ b/examples/screenshots/no-imgui-extent_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a5be8a506039eb509a2bac530eb723847fb2af21de17166dff08e2bf6d5d0d6 -size 149532 +oid sha256:c2ffe0a8d625322cc22d2abdde80a3f179f01552dde974bbbd49f9e371ab39aa +size 138936 diff --git a/examples/screenshots/no-imgui-rect_layout.png b/examples/screenshots/no-imgui-rect_layout.png index 928e5c391..16d1ff446 100644 --- a/examples/screenshots/no-imgui-rect_layout.png +++ b/examples/screenshots/no-imgui-rect_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a5be8a506039eb509a2bac530eb723847fb2af21de17166dff08e2bf6d5d0d6 -size 149532 +oid sha256:c2ffe0a8d625322cc22d2abdde80a3f179f01552dde974bbbd49f9e371ab39aa +size 138936 From acff8cd57e894a16857c3a31b7e5e7719f0ccf47 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 13 Mar 2025 23:33:29 -0400 Subject: [PATCH 78/82] black --- fastplotlib/graphics/text.py | 20 ++++++++++---------- fastplotlib/layouts/_figure.py | 4 +--- fastplotlib/layouts/_rect.py | 20 +++++++++++++++----- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 4a8a33543..e3794743a 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -79,16 +79,16 @@ def __init__( self._outline_thickness = TextOutlineThickness(outline_thickness) world_object = pygfx.Text( - text=self.text, - font_size=self.font_size, - screen_space=screen_space, - anchor=anchor, - material=pygfx.TextMaterial( - color=self.face_color, - outline_color=self.outline_color, - outline_thickness=self.outline_thickness, - pick_write=True, - ), + text=self.text, + font_size=self.font_size, + screen_space=screen_space, + anchor=anchor, + material=pygfx.TextMaterial( + color=self.face_color, + outline_color=self.outline_color, + outline_thickness=self.outline_thickness, + pick_write=True, + ), ) self._set_world_object(world_object) diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 11e937ae8..a5cb83463 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -133,9 +133,7 @@ def __init__( else: if not all(isinstance(v, (int, np.integer)) for v in shape): - raise TypeError( - "shape argument must be a tuple[n_rows, n_cols]" - ) + raise TypeError("shape argument must be a tuple[n_rows, n_cols]") n_subplots = shape[0] * shape[1] layout_mode = "grid" diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index 20b61ccff..aa84ee8a2 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -24,7 +24,9 @@ def _set(self, rect): rect = np.asarray(rect) for val, name in zip(rect, ["x-position", "y-position", "width", "height"]): if val < 0: - raise ValueError(f"Invalid rect value < 0: {rect}\n All values must be non-negative.") + raise ValueError( + f"Invalid rect value < 0: {rect}\n All values must be non-negative." + ) if (rect[2:] <= 1).all(): # fractional bbox self._set_from_fract(rect) @@ -42,9 +44,13 @@ def _set_from_fract(self, rect): # check that widths, heights are valid: if rect[0] + rect[2] > 1: - raise ValueError(f"invalid fractional rect: {rect}\n x + width > 1: {rect[0]} + {rect[2]} > 1") + raise ValueError( + f"invalid fractional rect: {rect}\n x + width > 1: {rect[0]} + {rect[2]} > 1" + ) if rect[1] + rect[3] > 1: - raise ValueError(f"invalid fractional rect: {rect}\n y + height > 1: {rect[1]} + {rect[3]} > 1") + raise ValueError( + f"invalid fractional rect: {rect}\n y + height > 1: {rect[1]} + {rect[3]} > 1" + ) # assign values to the arrays, don't just change the reference self._rect_frac[:] = rect @@ -57,9 +63,13 @@ def _set_from_screen_space(self, rect): # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 # check that widths, heights are valid if rect[0] + rect[2] > cw: - raise ValueError(f"invalid rect: {rect}\n x + width > canvas width: {rect[0]} + {rect[2]} > {cw}") + raise ValueError( + f"invalid rect: {rect}\n x + width > canvas width: {rect[0]} + {rect[2]} > {cw}" + ) if rect[1] + rect[3] > ch: - raise ValueError(f"invalid rect: {rect}\n y + height > canvas height: {rect[1]} + {rect[3]} >{ch}") + raise ValueError( + f"invalid rect: {rect}\n y + height > canvas height: {rect[1]} + {rect[3]} >{ch}" + ) self._rect_frac[:] = rect / mult self._rect_screen_space[:] = rect From 89ee9606bf65ae551562820da781cf17f0e80053 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 13 Mar 2025 23:59:20 -0400 Subject: [PATCH 79/82] newer black really was an extra comma for some reason --- fastplotlib/layouts/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index beec8dd39..866c26aa3 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -109,7 +109,7 @@ def create_controller( def get_extents_from_grid( - shape: tuple[int, int] + shape: tuple[int, int], ) -> list[tuple[float, float, float, float]]: """create fractional extents from a given grid shape""" x_min = np.arange(0, 1, (1 / shape[1])) From 1b69eaafdfd57fca68d1c69ec01e1138b87f2bd7 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 14 Mar 2025 00:44:23 -0400 Subject: [PATCH 80/82] update example --- examples/machine_learning/kmeans.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/examples/machine_learning/kmeans.py b/examples/machine_learning/kmeans.py index 620fa15fb..0aae8fdae 100644 --- a/examples/machine_learning/kmeans.py +++ b/examples/machine_learning/kmeans.py @@ -3,6 +3,9 @@ =================================== Example showing how you can perform K-Means clustering on the MNIST dataset. + +Use WASD keys on your keyboard to fly through the data in PCA space. +Use the mouse pointer to select points. """ # test_example = false @@ -29,17 +32,17 @@ # iterate through each subplot for i, subplot in enumerate(fig_data): # reshape each image to (8, 8) - subplot.add_image(data[i].reshape(8,8), cmap="gray", interpolation="linear") + subplot.add_image(data[i].reshape(8, 8), cmap="gray", interpolation="linear") # add the label as a title - subplot.set_title(f"Label: {labels[i]}") + subplot.title = f"Label: {labels[i]}" # turn off the axes and toolbar subplot.axes.visible = False - subplot.toolbar = False + subplot.toolbar = False fig_data.show() # project the data from 64 dimensions down to the number of unique digits -n_digits = len(np.unique(labels)) # 10 +n_digits = len(np.unique(labels)) # 10 reduced_data = PCA(n_components=n_digits).fit_transform(data) # (1797, 10) @@ -53,17 +56,17 @@ # plot the kmeans result and corresponding original image figure = fpl.Figure( - shape=(1,2), - size=(700, 400), + shape=(1, 2), + size=(700, 560), cameras=["3d", "2d"], - controller_types=[["fly", "panzoom"]] + controller_types=["fly", "panzoom"] ) -# set the axes to False -figure[0, 0].axes.visible = False +# set the axes to False in the image subplot figure[0, 1].axes.visible = False -figure[0, 0].set_title(f"K-means clustering of PCA-reduced data") +figure[0, 0].title = "k-means clustering of PCA-reduced data" +figure[0, 1].title = "handwritten digit" # plot the centroids figure[0, 0].add_scatter( @@ -94,6 +97,7 @@ digit_scatter.colors[ix] = "magenta" digit_scatter.sizes[ix] = 10 + # define event handler to update the selected data point @digit_scatter.add_event_handler("pointer_enter") def update(ev): @@ -110,8 +114,10 @@ def update(ev): # update digit fig figure[0, 1]["digit"].data = data[ix].reshape(8, 8) + figure.show() + # NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively # please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": From bc4b634987edc09bc0f86fd22d10f7313902dd39 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 14 Mar 2025 00:44:37 -0400 Subject: [PATCH 81/82] underline --- examples/window_layouts/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/window_layouts/README.rst b/examples/window_layouts/README.rst index 320a72b72..3c7df2366 100644 --- a/examples/window_layouts/README.rst +++ b/examples/window_layouts/README.rst @@ -1,2 +1,2 @@ WindowLayout Examples -=================== +===================== From f6263a324e8f9d939bfb9a25d165ca998010b060 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 14 Mar 2025 18:15:55 -0400 Subject: [PATCH 82/82] docstring, better exception messages --- fastplotlib/layouts/_engine.py | 10 ++++++++-- fastplotlib/layouts/_figure.py | 15 +++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 684417e21..877a7fbab 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -284,7 +284,10 @@ def set_rect(self, subplot: Subplot, rect: tuple | list | np.ndarray): the subplot to set the rect of rect: (x, y, w, h) - as absolute pixels or fractional + as absolute pixels or fractional. + If width & height <= 1 the rect is assumed to be fractional. + Conversely, if width & height > 1 the rect is assumed to be in absolute pixels. + width & height must be > 0. Negative values are not allowed. """ @@ -308,7 +311,10 @@ def set_extent(self, subplot: Subplot, extent: tuple | list | np.ndarray): the subplot to set the extent of extent: (xmin, xmax, ymin, ymax) - as absolute pixels or fractional + as absolute pixels or fractional. + If xmax & ymax <= 1 the extent is assumed to be fractional. + Conversely, if xmax & ymax > 1 the extent is assumed to be in absolute pixels. + Negative values are not allowed. xmax - xmin & ymax - ymin must be > 0. """ diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index a5cb83463..e1822eb64 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -61,11 +61,18 @@ def __init__( rects: list of tuples or arrays list of rects (x, y, width, height) that define the subplots. - rects can be defined in absolute pixels or as a fraction of the canvas + rects can be defined in absolute pixels or as a fraction of the canvas. + If width & height <= 1 the rect is assumed to be fractional. + Conversely, if width & height > 1 the rect is assumed to be in absolute pixels. + width & height must be > 0. Negative values are not allowed. extents: list of tuples or arrays list of extents (xmin, xmax, ymin, ymax) that define the subplots. extents can be defined in absolute pixels or as a fraction of the canvas. + If xmax & ymax <= 1 the extent is assumed to be fractional. + Conversely, if xmax & ymax > 1 the extent is assumed to be in absolute pixels. + Negative values are not allowed. xmax - xmin & ymax - ymin must be > 0. + If both ``rects`` and ``extents`` are provided, then ``rects`` takes precedence over ``extents``, i.e. ``extents`` is ignored when ``rects`` are also provided. @@ -146,7 +153,7 @@ def __init__( subplot_names = np.asarray(names).flatten() if subplot_names.size != n_subplots: raise ValueError( - "must provide same number of subplot `names` as specified by Figure `shape`" + f"must provide same number of subplot `names` as specified by shape, extents, or rects: {n_subplots}" ) else: if layout_mode == "grid": @@ -206,7 +213,7 @@ def __init__( if not subplot_controllers.size == n_subplots: raise ValueError( f"number of controllers passed must be the same as the number of subplots specified " - f"by shape: {n_subplots}. You have passed: {subplot_controllers.size} controllers" + f"by shape, extents, or rects: {n_subplots}. You have passed: {subplot_controllers.size} controllers" ) from None for index in range(n_subplots): @@ -278,7 +285,7 @@ def __init__( if controller_ids.size != n_subplots: raise ValueError( - "Number of controller_ids does not match the number of subplots" + f"Number of controller_ids does not match the number of subplots: {n_subplots}" ) if controller_types is None: 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