From 598dd8b2fde21a44035a547b8ae7df28277ce542 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 1 May 2025 23:29:34 -0400 Subject: [PATCH 1/4] proof of concept --- examples/reference_spaces/line_scales.py | 24 +++++ fastplotlib/graphics/_axes.py | 48 +++++----- fastplotlib/graphics/_base.py | 6 +- fastplotlib/layouts/_graphic_methods_mixin.py | 31 ++++--- fastplotlib/layouts/_plot_area.py | 47 +++++++++- fastplotlib/layouts/_reference_space.py | 90 +++++++++++++++++++ scripts/generate_add_graphic_methods.py | 11 ++- 7 files changed, 212 insertions(+), 45 deletions(-) create mode 100644 examples/reference_spaces/line_scales.py create mode 100644 fastplotlib/layouts/_reference_space.py diff --git a/examples/reference_spaces/line_scales.py b/examples/reference_spaces/line_scales.py new file mode 100644 index 000000000..585e7e276 --- /dev/null +++ b/examples/reference_spaces/line_scales.py @@ -0,0 +1,24 @@ +import numpy as np +import fastplotlib as fpl + + +xs = np.linspace(0, 10 * np.pi, 1000) +ys = np.sin(xs) + +ys100 = ys * 1000 + +l1 = np.column_stack([xs, ys]) +l2 = np.column_stack([xs, ys100]) + +fig = fpl.Figure(size=(500, 400)) + +fig[0, 0].add_line(l1) +fig.show(maintain_aspect=False) +fig[0, 0].auto_scale(zoom=0.4) + +rs = fig[0, 0].add_reference_space(scale=(1, 500, 1)) +l2 = fig[0, 0].add_line(l2, reference_space=rs, colors="r") +l2.add_axes(rs) +l2.axes.y.line.material.color = "r" + +fpl.loop.run() diff --git a/fastplotlib/graphics/_axes.py b/fastplotlib/graphics/_axes.py index 10774fc2a..1895406b0 100644 --- a/fastplotlib/graphics/_axes.py +++ b/fastplotlib/graphics/_axes.py @@ -144,7 +144,7 @@ def yz(self) -> Grid: class Axes: def __init__( self, - plot_area, + reference_space, intersection: tuple[int, int, int] | None = None, x_kwargs: dict = None, y_kwargs: dict = None, @@ -157,7 +157,7 @@ def __init__( [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] ), ): - self._plot_area = plot_area + self._reference_space = reference_space if x_kwargs is None: x_kwargs = dict() @@ -193,20 +193,20 @@ def __init__( self.x.end_pos = 100, 0, 0 self.x.start_value = self.x.start_pos[0] - offset[0] statsx = self.x.update( - self._plot_area.camera, self._plot_area.viewport.logical_size + self._reference_space.camera, self._reference_space.viewport.logical_size ) self.y.start_pos = 0, 0, 0 self.y.end_pos = 0, 100, 0 self.y.start_value = self.y.start_pos[1] - offset[1] statsy = self.y.update( - self._plot_area.camera, self._plot_area.viewport.logical_size + self._reference_space.camera, self._reference_space.viewport.logical_size ) self.z.start_pos = 0, 0, 0 self.z.end_pos = 0, 0, 100 self.z.start_value = self.z.start_pos[1] - offset[2] - self.z.update(self._plot_area.camera, self._plot_area.viewport.logical_size) + self.z.update(self._reference_space.camera, self._reference_space.viewport.logical_size) # world object for the rulers + grids self._world_object = pygfx.Group() @@ -219,7 +219,7 @@ def __init__( ) # set z ruler invisible for orthographic projections for now - if self._plot_area.camera.fov == 0: + if self._reference_space.camera.fov == 0: # TODO: allow any orientation in the future even for orthographic projections self.z.visible = False @@ -251,7 +251,7 @@ def __init__( self._grids = Grids(**_grids) self.world_object.add(self._grids) - if self._plot_area.camera.fov == 0: + if self._reference_space.camera.fov == 0: # orthographic projection, place grids far away self._grids.local.z = -1000 @@ -382,13 +382,13 @@ def update_using_bbox(self, bbox): """ # flip axes if camera scale is flipped - if self._plot_area.camera.local.scale_x < 0: + if self._reference_space.camera.local.scale_x < 0: bbox[0, 0], bbox[1, 0] = bbox[1, 0], bbox[0, 0] - if self._plot_area.camera.local.scale_y < 0: + if self._reference_space.camera.local.scale_y < 0: bbox[0, 1], bbox[1, 1] = bbox[1, 1], bbox[0, 1] - if self._plot_area.camera.local.scale_z < 0: + if self._reference_space.camera.local.scale_z < 0: bbox[0, 2], bbox[1, 2] = bbox[1, 2], bbox[0, 2] if self.intersection is None: @@ -413,8 +413,8 @@ def update_using_camera(self): if not self.visible: return - if self._plot_area.camera.fov == 0: - xpos, ypos, width, height = self._plot_area.viewport.rect + if self._reference_space.camera.fov == 0: + xpos, ypos, width, height = self._reference_space.viewport.rect # orthographic projection, get ranges using inverse # get range of screen space by getting the corners @@ -442,8 +442,8 @@ def update_using_camera(self): # self.y.local.rotation # ) - min_vals = self._plot_area.map_screen_to_world((xmin, ymin)) - max_vals = self._plot_area.map_screen_to_world((xmax, ymax)) + min_vals = self._reference_space.map_screen_to_world((xmin, ymin)) + max_vals = self._reference_space.map_screen_to_world((xmax, ymax)) if min_vals is None or max_vals is None: return @@ -462,14 +462,14 @@ def update_using_camera(self): else: # set ruler start and end positions based on scene bbox - bbox = self._plot_area._fpl_graphics_scene.get_world_bounding_box() + bbox = self._reference_space._fpl_graphics_scene.get_world_bounding_box() if self.intersection is None: - if self._plot_area.camera.fov == 0: + if self._reference_space.camera.fov == 0: # place the ruler close to the left and bottom edges of the viewport # TODO: determine this for perspective projections xscreen_10, yscreen_10 = xpos + (width * 0.1), ypos + (height * 0.9) - intersection = self._plot_area.map_screen_to_world( + intersection = self._reference_space.map_screen_to_world( (xscreen_10, yscreen_10) ) else: @@ -502,7 +502,7 @@ def update(self, bbox, intersection): world_x_10, world_y_10, world_z_10 = intersection # swap min and max for each dimension if necessary - if self._plot_area.camera.local.scale_y < 0: + if self._reference_space.camera.local.scale_y < 0: world_ymin, world_ymax = world_ymax, world_ymin self.y.tick_side = "right" # swap tick side self.x.tick_side = "right" @@ -510,7 +510,7 @@ def update(self, bbox, intersection): self.y.tick_side = "left" self.x.tick_side = "right" - if self._plot_area.camera.local.scale_x < 0: + if self._reference_space.camera.local.scale_x < 0: world_xmin, world_xmax = world_xmax, world_xmin self.x.tick_side = "left" @@ -519,7 +519,7 @@ def update(self, bbox, intersection): self.x.start_value = self.x.start_pos[0] - self.offset[0] statsx = self.x.update( - self._plot_area.camera, self._plot_area.viewport.logical_size + self._reference_space.camera, self._reference_space.viewport.logical_size ) self.y.start_pos = world_x_10, world_ymin, world_z_10 @@ -527,16 +527,16 @@ def update(self, bbox, intersection): self.y.start_value = self.y.start_pos[1] - self.offset[1] statsy = self.y.update( - self._plot_area.camera, self._plot_area.viewport.logical_size + self._reference_space.camera, self._reference_space.viewport.logical_size ) - if self._plot_area.camera.fov != 0: + if self._reference_space.camera.fov != 0: self.z.start_pos = world_x_10, world_y_10, world_zmin self.z.end_pos = world_x_10, world_y_10, world_zmax self.z.start_value = self.z.start_pos[2] - self.offset[2] statsz = self.z.update( - self._plot_area.camera, self._plot_area.viewport.logical_size + self._reference_space.camera, self._reference_space.viewport.logical_size ) major_step_z = statsz["tick_step"] @@ -546,7 +546,7 @@ def update(self, bbox, intersection): self.grids.xy.major_step = major_step_x, major_step_y self.grids.xy.minor_step = 0.2 * major_step_x, 0.2 * major_step_y - if self._plot_area.camera.fov != 0: + if self._reference_space.camera.fov != 0: self.grids.xz.major_step = major_step_x, major_step_z self.grids.xz.minor_step = 0.2 * major_step_x, 0.2 * major_step_z diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index e115107b0..168f976c8 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -447,15 +447,15 @@ def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"): def axes(self) -> Axes: return self._axes - def add_axes(self): + def add_axes(self, reference_frame): """Add axes onto this Graphic""" if self._axes is not None: raise AttributeError("Axes already added onto this graphic") - self._axes = Axes(self._plot_area, offset=self.offset, grids=False) + self._axes = Axes(reference_frame, offset=self.offset, grids=False) self._axes.world_object.local.rotation = self.world_object.local.rotation - self._plot_area.scene.add(self.axes.world_object) + reference_frame.scene.add(self.axes.world_object) self._axes.update_using_bbox(self.world_object.get_world_bounding_box()) @property diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index a753eec73..d6eedeee5 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -15,11 +15,16 @@ def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic: else: center = False + if "reference_space" in kwargs.keys(): + reference_space = kwargs.pop("reference_space") + else: + reference_space = 0 + if "name" in kwargs.keys(): self._check_graphic_name_exists(kwargs["name"]) graphic = graphic_class(*args, **kwargs) - self.add_graphic(graphic, center=center) + self.add_graphic(graphic, center=center, reference_space=reference_space) return graphic @@ -32,7 +37,7 @@ def add_image( interpolation: str = "nearest", cmap_interpolation: str = "linear", isolated_buffer: bool = True, - **kwargs, + **kwargs ) -> ImageGraphic: """ @@ -78,7 +83,7 @@ def add_image( interpolation, cmap_interpolation, isolated_buffer, - **kwargs, + **kwargs ) def add_line_collection( @@ -96,7 +101,7 @@ def add_line_collection( metadatas: Union[Sequence[Any], numpy.ndarray] = None, isolated_buffer: bool = True, kwargs_lines: list[dict] = None, - **kwargs, + **kwargs ) -> LineCollection: """ @@ -169,7 +174,7 @@ def add_line_collection( metadatas, isolated_buffer, kwargs_lines, - **kwargs, + **kwargs ) def add_line( @@ -183,7 +188,7 @@ def add_line( cmap_transform: Union[numpy.ndarray, Iterable] = None, isolated_buffer: bool = True, size_space: str = "screen", - **kwargs, + **kwargs ) -> LineGraphic: """ @@ -234,7 +239,7 @@ def add_line( cmap_transform, isolated_buffer, size_space, - **kwargs, + **kwargs ) def add_line_stack( @@ -253,7 +258,7 @@ def add_line_stack( separation: float = 10.0, separation_axis: str = "y", kwargs_lines: list[dict] = None, - **kwargs, + **kwargs ) -> LineStack: """ @@ -334,7 +339,7 @@ def add_line_stack( separation, separation_axis, kwargs_lines, - **kwargs, + **kwargs ) def add_scatter( @@ -349,7 +354,7 @@ def add_scatter( sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, uniform_size: bool = False, size_space: str = "screen", - **kwargs, + **kwargs ) -> ScatterGraphic: """ @@ -409,7 +414,7 @@ def add_scatter( sizes, uniform_size, size_space, - **kwargs, + **kwargs ) def add_text( @@ -422,7 +427,7 @@ def add_text( screen_space: bool = True, offset: tuple[float] = (0, 0, 0), anchor: str = "middle-center", - **kwargs, + **kwargs ) -> TextGraphic: """ @@ -473,5 +478,5 @@ def add_text( screen_space, offset, anchor, - **kwargs, + **kwargs ) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 2934e0589..1a25fd18b 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -8,8 +8,9 @@ from pylinalg import vec_transform, vec_unproject from rendercanvas import BaseRenderCanvas +from ._reference_space import ReferenceSpace from ._utils import create_controller -from ..graphics._base import Graphic +from ..graphics import Graphic from ..graphics.selectors._base_selector import BaseSelector from ._graphic_methods_mixin import GraphicMethodsMixin from ..legends import Legend @@ -115,6 +116,8 @@ def __init__( self._background = pygfx.Background(None, self._background_material) self.scene.add(self._background) + self._reference_spaces: list[ReferenceSpace] = list() + def get_figure(self, obj=None): """Get Figure instance that contains this plot area""" if obj is None: @@ -272,6 +275,35 @@ def background_color(self, colors: str | tuple[float]): """1, 2, or 4 colors, each color must be acceptable by pygfx.Color""" self._background_material.set_colors(*colors) + @property + def reference_spaces(self) -> tuple[ReferenceSpace, ...]: + return tuple(self._reference_spaces) + + def add_reference_space( + self, + position: tuple[float, float, float] = (0., 0., 0.), + scale: tuple[float, float, float] = (1., 1., 1.), + name: str | None = None + ) -> ReferenceSpace: + camera = pygfx.PerspectiveCamera() + + state = self.camera.get_state() + camera.set_state(state) + + # camera.world.position = position + camera.world.scale = scale + camera.maintain_aspect = False + + scene = pygfx.Scene() + + controller = pygfx.PanZoomController(camera) + controller.register_events(self.viewport) + + reference_space = ReferenceSpace(scene, camera, controller, self.viewport, name) + self._reference_spaces.append(reference_space) + + return reference_space + def map_screen_to_world( self, pos: tuple[float, float] | pygfx.PointerEvent, allow_outside: bool = False ) -> np.ndarray | None: @@ -314,6 +346,9 @@ def _render(self): # does not flush, flush must be implemented in user-facing Plot objects self.viewport.render(self.scene, self.camera) + for reference_space in self.reference_spaces: + self.viewport.render(reference_space.scene, reference_space.camera) + for child in self.children: child._render() @@ -393,7 +428,7 @@ def remove_animation(self, func): if func in self._animate_funcs_post: self._animate_funcs_post.remove(func) - def add_graphic(self, graphic: Graphic, center: bool = True): + def add_graphic(self, graphic: Graphic, center: bool = True, reference_space: ReferenceSpace | int | str = 0): """ Add a Graphic to the scene @@ -413,7 +448,7 @@ def add_graphic(self, graphic: Graphic, center: bool = True): self._fpl_graphics_scene.add(graphic.world_object) return - self._add_or_insert_graphic(graphic=graphic, center=center, action="add") + self._add_or_insert_graphic(graphic=graphic, center=center, action="add", reference_space=reference_space) if self.camera.fov == 0: # for orthographic positions stack objects along the z-axis @@ -469,6 +504,7 @@ def _add_or_insert_graphic( center: bool = True, action: str = Literal["insert", "add"], index: int = 0, + reference_space: ReferenceSpace | str | int = 0, ): """Private method to handle inserting or adding a graphic to a PlotArea.""" if not isinstance(graphic, Graphic): @@ -489,7 +525,10 @@ def _add_or_insert_graphic( elif isinstance(graphic, Graphic): obj_list = self._graphics - self._fpl_graphics_scene.add(graphic.world_object) + if isinstance(reference_space, ReferenceSpace): + reference_space.scene.add(graphic.world_object) + else: + self._fpl_graphics_scene.add(graphic.world_object) else: raise TypeError("graphic must be of type Graphic | BaseSelector | Legend") diff --git a/fastplotlib/layouts/_reference_space.py b/fastplotlib/layouts/_reference_space.py new file mode 100644 index 000000000..7b7da609d --- /dev/null +++ b/fastplotlib/layouts/_reference_space.py @@ -0,0 +1,90 @@ +import numpy as np + +import pygfx +from pylinalg import vec_transform, vec_unproject + +from ..graphics import Graphic + + +class ReferenceSpace: + def __init__( + self, + scene: pygfx.Scene, + camera: pygfx.Camera, + controller: pygfx.Controller, + viewport: pygfx.Viewport, + name: str | None = None, + ): + self._scene = scene + self._camera = camera + self._controller = controller + self.viewport = viewport + self._name = name + + self._graphics: list[Graphic] = list() + + @property + def name(self) -> str: + return self._name + + @property + def scene(self) -> pygfx.Scene: + return self._scene + + @property + def camera(self) -> pygfx.Camera: + return self._camera + + # @property + # def controller(self): + # same controller for all reference spaces in one PlotArea I think? + # Use key events to add or remove a camera from the PlotArea dynamically? + # pass + + def auto_scale(self): + pass + + def center(self): + pass + + @property + def graphics(self) -> np.ndarray[Graphic]: + graphics = np.asarray(self._graphics) + graphics.flags.writeable = False + return graphics + + def map_screen_to_world( + self, pos: tuple[float, float] | pygfx.PointerEvent, allow_outside: bool = False + ) -> np.ndarray | None: + """ + Map screen position to world position + + Parameters + ---------- + pos: (float, float) | pygfx.PointerEvent + ``(x, y)`` screen coordinates, or ``pygfx.PointerEvent`` + + """ + if isinstance(pos, pygfx.PointerEvent): + pos = pos.x, pos.y + + if not allow_outside and not self.viewport.is_inside(*pos): + return None + + vs = self.viewport.logical_size + + # get position relative to viewport + pos_rel = ( + pos[0] - self.viewport.rect[0], + pos[1] - self.viewport.rect[1], + ) + + # convert screen position to NDC + pos_ndc = (pos_rel[0] / vs[0] * 2 - 1, -(pos_rel[1] / vs[1] * 2 - 1), 0) + + # get world position + pos_ndc += vec_transform(self.camera.world.position, self.camera.camera_matrix) + pos_world = vec_unproject(pos_ndc[:2], self.camera.camera_matrix) + + # default z is zero for now + return np.array([*pos_world[:2], 0]) diff --git a/scripts/generate_add_graphic_methods.py b/scripts/generate_add_graphic_methods.py index 533ae77c6..1080d99da 100644 --- a/scripts/generate_add_graphic_methods.py +++ b/scripts/generate_add_graphic_methods.py @@ -19,6 +19,9 @@ for name, obj in inspect.getmembers(graphics): if inspect.isclass(obj): + if obj.__name__ == "Graphic": + # skip base class + continue modules.append(obj) @@ -42,10 +45,16 @@ def generate_add_graphics_methods(): f.write(" center = kwargs.pop('center')\n") f.write(" else:\n") f.write(" center = False\n\n") + + f.write(" if 'reference_space' in kwargs.keys():\n") + f.write(" reference_space = kwargs.pop('reference_space')\n") + f.write(" else:\n") + f.write(" reference_space = 0\n\n") + f.write(" if 'name' in kwargs.keys():\n") f.write(" self._check_graphic_name_exists(kwargs['name'])\n\n") f.write(" graphic = graphic_class(*args, **kwargs)\n") - f.write(" self.add_graphic(graphic, center=center)\n\n") + f.write(" self.add_graphic(graphic, center=center, reference_space=reference_space)\n\n") f.write(" return graphic\n\n") for m in modules: From 4599fa74397e7614cb8a1225bdc1fa27836f4107 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 1 May 2025 23:43:41 -0400 Subject: [PATCH 2/4] controller property --- fastplotlib/layouts/_reference_space.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/fastplotlib/layouts/_reference_space.py b/fastplotlib/layouts/_reference_space.py index 7b7da609d..79c396af9 100644 --- a/fastplotlib/layouts/_reference_space.py +++ b/fastplotlib/layouts/_reference_space.py @@ -35,11 +35,9 @@ def scene(self) -> pygfx.Scene: def camera(self) -> pygfx.Camera: return self._camera - # @property - # def controller(self): - # same controller for all reference spaces in one PlotArea I think? - # Use key events to add or remove a camera from the PlotArea dynamically? - # pass + @property + def controller(self) -> pygfx.Controller: + return self._controller def auto_scale(self): pass From a1934d5ab98649a4e6eeee019990d20b0f3862e8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 7 May 2025 21:46:50 -0400 Subject: [PATCH 3/4] rename --- fastplotlib/layouts/_plot_area.py | 49 ++++++++++++++----------- fastplotlib/layouts/_reference_space.py | 2 +- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 1a25fd18b..f3a791117 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -8,7 +8,7 @@ from pylinalg import vec_transform, vec_unproject from rendercanvas import BaseRenderCanvas -from ._reference_space import ReferenceSpace +from ._reference_space import ReferenceFrame from ._utils import create_controller from ..graphics import Graphic from ..graphics.selectors._base_selector import BaseSelector @@ -116,7 +116,7 @@ def __init__( self._background = pygfx.Background(None, self._background_material) self.scene.add(self._background) - self._reference_spaces: list[ReferenceSpace] = list() + self._reference_frames: list[ReferenceFrame] = list() def get_figure(self, obj=None): """Get Figure instance that contains this plot area""" @@ -276,33 +276,40 @@ def background_color(self, colors: str | tuple[float]): self._background_material.set_colors(*colors) @property - def reference_spaces(self) -> tuple[ReferenceSpace, ...]: - return tuple(self._reference_spaces) + def reference_frames(self) -> tuple[ReferenceFrame, ...]: + return tuple(self._reference_frames) - def add_reference_space( + def add_reference_frame( self, - position: tuple[float, float, float] = (0., 0., 0.), - scale: tuple[float, float, float] = (1., 1., 1.), + position: tuple[float, float, float] | None = None, + scale: tuple[float, float, float] | None = None, + controller_type: str = None, + controller_include_state=None, + controller_exclude_state=None, name: str | None = None - ) -> ReferenceSpace: + ) -> ReferenceFrame: camera = pygfx.PerspectiveCamera() state = self.camera.get_state() camera.set_state(state) - # camera.world.position = position - camera.world.scale = scale - camera.maintain_aspect = False + if position is not None: + camera.world.position = position + if scale is not None: + camera.world.scale = scale + + camera.maintain_aspect = self.camera.maintain_aspect scene = pygfx.Scene() - controller = pygfx.PanZoomController(camera) + controller = pygfx.PanZoomController() + controller.add_camera(camera, include_state=controller_include_state, exclude_state=controller_exclude_state) controller.register_events(self.viewport) - reference_space = ReferenceSpace(scene, camera, controller, self.viewport, name) - self._reference_spaces.append(reference_space) + ref_frame = ReferenceFrame(scene, camera, controller, self.viewport, name) + self._reference_frames.append(ref_frame) - return reference_space + return ref_frame def map_screen_to_world( self, pos: tuple[float, float] | pygfx.PointerEvent, allow_outside: bool = False @@ -346,7 +353,7 @@ def _render(self): # does not flush, flush must be implemented in user-facing Plot objects self.viewport.render(self.scene, self.camera) - for reference_space in self.reference_spaces: + for reference_space in self.reference_frames: self.viewport.render(reference_space.scene, reference_space.camera) for child in self.children: @@ -428,7 +435,7 @@ def remove_animation(self, func): if func in self._animate_funcs_post: self._animate_funcs_post.remove(func) - def add_graphic(self, graphic: Graphic, center: bool = True, reference_space: ReferenceSpace | int | str = 0): + def add_graphic(self, graphic: Graphic, center: bool = True, reference_frame: ReferenceFrame | int | str = 0): """ Add a Graphic to the scene @@ -448,7 +455,7 @@ def add_graphic(self, graphic: Graphic, center: bool = True, reference_space: Re self._fpl_graphics_scene.add(graphic.world_object) return - self._add_or_insert_graphic(graphic=graphic, center=center, action="add", reference_space=reference_space) + self._add_or_insert_graphic(graphic=graphic, center=center, action="add", reference_frame=reference_frame) if self.camera.fov == 0: # for orthographic positions stack objects along the z-axis @@ -504,7 +511,7 @@ def _add_or_insert_graphic( center: bool = True, action: str = Literal["insert", "add"], index: int = 0, - reference_space: ReferenceSpace | str | int = 0, + reference_frame: ReferenceFrame | str | int = 0, ): """Private method to handle inserting or adding a graphic to a PlotArea.""" if not isinstance(graphic, Graphic): @@ -525,8 +532,8 @@ def _add_or_insert_graphic( elif isinstance(graphic, Graphic): obj_list = self._graphics - if isinstance(reference_space, ReferenceSpace): - reference_space.scene.add(graphic.world_object) + if isinstance(reference_frame, ReferenceFrame): + reference_frame.scene.add(graphic.world_object) else: self._fpl_graphics_scene.add(graphic.world_object) diff --git a/fastplotlib/layouts/_reference_space.py b/fastplotlib/layouts/_reference_space.py index 79c396af9..930769dd6 100644 --- a/fastplotlib/layouts/_reference_space.py +++ b/fastplotlib/layouts/_reference_space.py @@ -6,7 +6,7 @@ from ..graphics import Graphic -class ReferenceSpace: +class ReferenceFrame: def __init__( self, scene: pygfx.Scene, From 9bfd8164af9cc82b97385bbc524d2ad20b530ff3 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 7 May 2025 21:47:57 -0400 Subject: [PATCH 4/4] example --- examples/reference_spaces/line_scales.py | 41 ++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/examples/reference_spaces/line_scales.py b/examples/reference_spaces/line_scales.py index 585e7e276..5d07e28cd 100644 --- a/examples/reference_spaces/line_scales.py +++ b/examples/reference_spaces/line_scales.py @@ -1,6 +1,8 @@ import numpy as np import fastplotlib as fpl - +from fastplotlib.ui import DebugWindow +import pygfx +from icecream import ic xs = np.linspace(0, 10 * np.pi, 1000) ys = np.sin(xs) @@ -16,9 +18,42 @@ fig.show(maintain_aspect=False) fig[0, 0].auto_scale(zoom=0.4) -rs = fig[0, 0].add_reference_space(scale=(1, 500, 1)) +rs = fig[0, 0].add_reference_frame( + scale=(1, 500, 1), +) l2 = fig[0, 0].add_line(l2, reference_space=rs, colors="r") l2.add_axes(rs) -l2.axes.y.line.material.color = "r" +l2.axes.y.line.material.color = "m" + + +@fig.renderer.add_event_handler("key_down") +def change_y_scale(ev: pygfx.KeyboardEvent): + if ev.key != "1": + return + + rs.controller.remove_camera(rs.camera) + rs.controller.add_camera(rs.camera, include_state={"height"}) + + fig[0, 0].controller.enabled = False + + +@fig.renderer.add_event_handler("key_down") +def change_y_scale(ev: pygfx.KeyboardEvent): + if ev.key != "0": + return + + rs.controller.remove_camera(rs.camera) + rs.controller.add_camera(rs.camera) + fig[0, 0].controller.enabled = True + + +debug_objs = [ + fig[0, 0].camera.get_state, + rs.camera.get_state +] + +debug_window = DebugWindow(debug_objs) +fig.add_gui(debug_window) + fpl.loop.run()