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: