diff --git a/docs/source/api/graphic_features/Scale.rst b/docs/source/api/graphic_features/Scale.rst new file mode 100644 index 000000000..b0ef07a79 --- /dev/null +++ b/docs/source/api/graphic_features/Scale.rst @@ -0,0 +1,35 @@ +.. _api.Scale: + +Scale +***** + +===== +Scale +===== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Scale_api + + Scale + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Scale_api + + Scale.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Scale_api + + Scale.add_event_handler + Scale.block_events + Scale.clear_event_handlers + Scale.remove_event_handler + Scale.set_value + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index cd11544be..71268ddab 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -48,6 +48,7 @@ Graphic Features Name Offset Rotation + Scale Alpha AlphaMode Visible diff --git a/docs/source/api/graphics/Graphic.rst b/docs/source/api/graphics/Graphic.rst index da6424e3e..f94892949 100644 --- a/docs/source/api/graphics/Graphic.rst +++ b/docs/source/api/graphics/Graphic.rst @@ -30,7 +30,9 @@ Properties Graphic.offset Graphic.right_click_menu Graphic.rotation + Graphic.scale Graphic.supported_events + Graphic.tooltip_format Graphic.visible Graphic.world_object @@ -42,6 +44,9 @@ Methods Graphic.add_axes Graphic.add_event_handler Graphic.clear_event_handlers + Graphic.format_pick_info + Graphic.map_model_to_world + Graphic.map_world_to_model Graphic.remove_event_handler Graphic.rotate diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst index 457ba27ee..e6d02c54b 100644 --- a/docs/source/api/graphics/ImageGraphic.rst +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -34,7 +34,9 @@ Properties ImageGraphic.offset ImageGraphic.right_click_menu ImageGraphic.rotation + ImageGraphic.scale ImageGraphic.supported_events + ImageGraphic.tooltip_format ImageGraphic.visible ImageGraphic.vmax ImageGraphic.vmin @@ -52,6 +54,9 @@ Methods ImageGraphic.add_polygon_selector ImageGraphic.add_rectangle_selector ImageGraphic.clear_event_handlers + ImageGraphic.format_pick_info + ImageGraphic.map_model_to_world + ImageGraphic.map_world_to_model ImageGraphic.remove_event_handler ImageGraphic.reset_vmin_vmax ImageGraphic.rotate diff --git a/docs/source/api/graphics/ImageVolumeGraphic.rst b/docs/source/api/graphics/ImageVolumeGraphic.rst index 8adbc7ac7..8031f12f1 100644 --- a/docs/source/api/graphics/ImageVolumeGraphic.rst +++ b/docs/source/api/graphics/ImageVolumeGraphic.rst @@ -37,11 +37,13 @@ Properties ImageVolumeGraphic.plane ImageVolumeGraphic.right_click_menu ImageVolumeGraphic.rotation + ImageVolumeGraphic.scale ImageVolumeGraphic.shininess ImageVolumeGraphic.step_size ImageVolumeGraphic.substep_size ImageVolumeGraphic.supported_events ImageVolumeGraphic.threshold + ImageVolumeGraphic.tooltip_format ImageVolumeGraphic.visible ImageVolumeGraphic.vmax ImageVolumeGraphic.vmin @@ -55,6 +57,9 @@ Methods ImageVolumeGraphic.add_axes ImageVolumeGraphic.add_event_handler ImageVolumeGraphic.clear_event_handlers + ImageVolumeGraphic.format_pick_info + ImageVolumeGraphic.map_model_to_world + ImageVolumeGraphic.map_world_to_model ImageVolumeGraphic.remove_event_handler ImageVolumeGraphic.reset_vmin_vmax ImageVolumeGraphic.rotate diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst index ffbb52f2b..5d0603ab7 100644 --- a/docs/source/api/graphics/LineCollection.rst +++ b/docs/source/api/graphics/LineCollection.rst @@ -38,8 +38,10 @@ Properties LineCollection.right_click_menu LineCollection.rotation LineCollection.rotations + LineCollection.scale LineCollection.supported_events LineCollection.thickness + LineCollection.tooltip_format LineCollection.visible LineCollection.visibles LineCollection.world_object @@ -57,6 +59,9 @@ Methods LineCollection.add_polygon_selector LineCollection.add_rectangle_selector LineCollection.clear_event_handlers + LineCollection.format_pick_info + LineCollection.map_model_to_world + LineCollection.map_world_to_model LineCollection.remove_event_handler LineCollection.remove_graphic LineCollection.rotate diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index ddcb00c41..428e8ef56 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -33,9 +33,11 @@ Properties LineGraphic.offset LineGraphic.right_click_menu LineGraphic.rotation + LineGraphic.scale LineGraphic.size_space LineGraphic.supported_events LineGraphic.thickness + LineGraphic.tooltip_format LineGraphic.visible LineGraphic.world_object @@ -51,6 +53,9 @@ Methods LineGraphic.add_polygon_selector LineGraphic.add_rectangle_selector LineGraphic.clear_event_handlers + LineGraphic.format_pick_info + LineGraphic.map_model_to_world + LineGraphic.map_world_to_model LineGraphic.remove_event_handler LineGraphic.rotate diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst index 4373454be..e7ac21343 100644 --- a/docs/source/api/graphics/LineStack.rst +++ b/docs/source/api/graphics/LineStack.rst @@ -38,8 +38,10 @@ Properties LineStack.right_click_menu LineStack.rotation LineStack.rotations + LineStack.scale LineStack.supported_events LineStack.thickness + LineStack.tooltip_format LineStack.visible LineStack.visibles LineStack.world_object @@ -57,6 +59,9 @@ Methods LineStack.add_polygon_selector LineStack.add_rectangle_selector LineStack.clear_event_handlers + LineStack.format_pick_info + LineStack.map_model_to_world + LineStack.map_world_to_model LineStack.remove_event_handler LineStack.remove_graphic LineStack.rotate diff --git a/docs/source/api/graphics/MeshGraphic.rst b/docs/source/api/graphics/MeshGraphic.rst index 5e2c5dac5..ec27f1e4e 100644 --- a/docs/source/api/graphics/MeshGraphic.rst +++ b/docs/source/api/graphics/MeshGraphic.rst @@ -38,7 +38,9 @@ Properties MeshGraphic.positions MeshGraphic.right_click_menu MeshGraphic.rotation + MeshGraphic.scale MeshGraphic.supported_events + MeshGraphic.tooltip_format MeshGraphic.visible MeshGraphic.world_object @@ -50,6 +52,9 @@ Methods MeshGraphic.add_axes MeshGraphic.add_event_handler MeshGraphic.clear_event_handlers + MeshGraphic.format_pick_info + MeshGraphic.map_model_to_world + MeshGraphic.map_world_to_model MeshGraphic.remove_event_handler MeshGraphic.rotate diff --git a/docs/source/api/graphics/PolygonGraphic.rst b/docs/source/api/graphics/PolygonGraphic.rst index f9446f425..94c75f999 100644 --- a/docs/source/api/graphics/PolygonGraphic.rst +++ b/docs/source/api/graphics/PolygonGraphic.rst @@ -39,7 +39,9 @@ Properties PolygonGraphic.positions PolygonGraphic.right_click_menu PolygonGraphic.rotation + PolygonGraphic.scale PolygonGraphic.supported_events + PolygonGraphic.tooltip_format PolygonGraphic.visible PolygonGraphic.world_object @@ -51,6 +53,9 @@ Methods PolygonGraphic.add_axes PolygonGraphic.add_event_handler PolygonGraphic.clear_event_handlers + PolygonGraphic.format_pick_info + PolygonGraphic.map_model_to_world + PolygonGraphic.map_world_to_model PolygonGraphic.remove_event_handler PolygonGraphic.rotate diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index 7f4336abe..cf8e1224d 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -40,9 +40,11 @@ Properties ScatterGraphic.point_rotations ScatterGraphic.right_click_menu ScatterGraphic.rotation + ScatterGraphic.scale ScatterGraphic.size_space ScatterGraphic.sizes ScatterGraphic.supported_events + ScatterGraphic.tooltip_format ScatterGraphic.visible ScatterGraphic.world_object @@ -54,6 +56,9 @@ Methods ScatterGraphic.add_axes ScatterGraphic.add_event_handler ScatterGraphic.clear_event_handlers + ScatterGraphic.format_pick_info + ScatterGraphic.map_model_to_world + ScatterGraphic.map_world_to_model ScatterGraphic.remove_event_handler ScatterGraphic.rotate diff --git a/docs/source/api/graphics/SurfaceGraphic.rst b/docs/source/api/graphics/SurfaceGraphic.rst index 385ce2432..228dbede1 100644 --- a/docs/source/api/graphics/SurfaceGraphic.rst +++ b/docs/source/api/graphics/SurfaceGraphic.rst @@ -39,7 +39,9 @@ Properties SurfaceGraphic.positions SurfaceGraphic.right_click_menu SurfaceGraphic.rotation + SurfaceGraphic.scale SurfaceGraphic.supported_events + SurfaceGraphic.tooltip_format SurfaceGraphic.visible SurfaceGraphic.world_object @@ -51,6 +53,9 @@ Methods SurfaceGraphic.add_axes SurfaceGraphic.add_event_handler SurfaceGraphic.clear_event_handlers + SurfaceGraphic.format_pick_info + SurfaceGraphic.map_model_to_world + SurfaceGraphic.map_world_to_model SurfaceGraphic.remove_event_handler SurfaceGraphic.rotate diff --git a/docs/source/api/graphics/TextGraphic.rst b/docs/source/api/graphics/TextGraphic.rst index 0de52942b..da4909686 100644 --- a/docs/source/api/graphics/TextGraphic.rst +++ b/docs/source/api/graphics/TextGraphic.rst @@ -34,8 +34,10 @@ Properties TextGraphic.outline_thickness TextGraphic.right_click_menu TextGraphic.rotation + TextGraphic.scale TextGraphic.supported_events TextGraphic.text + TextGraphic.tooltip_format TextGraphic.visible TextGraphic.world_object @@ -47,6 +49,9 @@ Methods TextGraphic.add_axes TextGraphic.add_event_handler TextGraphic.clear_event_handlers + TextGraphic.format_pick_info + TextGraphic.map_model_to_world + TextGraphic.map_world_to_model TextGraphic.remove_event_handler TextGraphic.rotate diff --git a/docs/source/api/graphics/VectorsGraphic.rst b/docs/source/api/graphics/VectorsGraphic.rst index 4a629f5db..ec7d891c0 100644 --- a/docs/source/api/graphics/VectorsGraphic.rst +++ b/docs/source/api/graphics/VectorsGraphic.rst @@ -32,7 +32,9 @@ Properties VectorsGraphic.positions VectorsGraphic.right_click_menu VectorsGraphic.rotation + VectorsGraphic.scale VectorsGraphic.supported_events + VectorsGraphic.tooltip_format VectorsGraphic.visible VectorsGraphic.world_object @@ -44,6 +46,9 @@ Methods VectorsGraphic.add_axes VectorsGraphic.add_event_handler VectorsGraphic.clear_event_handlers + VectorsGraphic.format_pick_info + VectorsGraphic.map_model_to_world + VectorsGraphic.map_world_to_model VectorsGraphic.remove_event_handler VectorsGraphic.rotate diff --git a/docs/source/api/layouts/figure.rst b/docs/source/api/layouts/figure.rst index e306710be..54e91b24f 100644 --- a/docs/source/api/layouts/figure.rst +++ b/docs/source/api/layouts/figure.rst @@ -28,8 +28,6 @@ Properties Figure.names Figure.renderer Figure.shape - Figure.show_tooltips - Figure.tooltip_manager Methods ~~~~~~~ diff --git a/docs/source/api/layouts/imgui_figure.rst b/docs/source/api/layouts/imgui_figure.rst index 959a98743..46e0c6ed3 100644 --- a/docs/source/api/layouts/imgui_figure.rst +++ b/docs/source/api/layouts/imgui_figure.rst @@ -31,8 +31,6 @@ Properties ImguiFigure.names ImguiFigure.renderer ImguiFigure.shape - ImguiFigure.show_tooltips - ImguiFigure.tooltip_manager Methods ~~~~~~~ diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index 93db00a2e..0916859b9 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -40,6 +40,7 @@ Properties Subplot.selectors Subplot.title Subplot.toolbar + Subplot.tooltip Subplot.viewport Methods @@ -67,8 +68,10 @@ Methods Subplot.clear_animations Subplot.delete_graphic Subplot.get_figure + Subplot.get_pick_info Subplot.insert_graphic Subplot.map_screen_to_world + Subplot.map_world_to_screen Subplot.remove_animation Subplot.remove_graphic diff --git a/docs/source/api/selectors/LinearRegionSelector.rst b/docs/source/api/selectors/LinearRegionSelector.rst index 35b5ae1f4..eb48497cd 100644 --- a/docs/source/api/selectors/LinearRegionSelector.rst +++ b/docs/source/api/selectors/LinearRegionSelector.rst @@ -35,8 +35,10 @@ Properties LinearRegionSelector.parent LinearRegionSelector.right_click_menu LinearRegionSelector.rotation + LinearRegionSelector.scale LinearRegionSelector.selection LinearRegionSelector.supported_events + LinearRegionSelector.tooltip_format LinearRegionSelector.vertex_color LinearRegionSelector.visible LinearRegionSelector.world_object @@ -49,9 +51,12 @@ Methods LinearRegionSelector.add_axes LinearRegionSelector.add_event_handler LinearRegionSelector.clear_event_handlers + LinearRegionSelector.format_pick_info LinearRegionSelector.get_selected_data LinearRegionSelector.get_selected_index LinearRegionSelector.get_selected_indices + LinearRegionSelector.map_model_to_world + LinearRegionSelector.map_world_to_model LinearRegionSelector.remove_event_handler LinearRegionSelector.rotate diff --git a/docs/source/api/selectors/LinearSelector.rst b/docs/source/api/selectors/LinearSelector.rst index 9cbe6fb26..2aa334748 100644 --- a/docs/source/api/selectors/LinearSelector.rst +++ b/docs/source/api/selectors/LinearSelector.rst @@ -35,8 +35,10 @@ Properties LinearSelector.parent LinearSelector.right_click_menu LinearSelector.rotation + LinearSelector.scale LinearSelector.selection LinearSelector.supported_events + LinearSelector.tooltip_format LinearSelector.vertex_color LinearSelector.visible LinearSelector.world_object @@ -49,9 +51,12 @@ Methods LinearSelector.add_axes LinearSelector.add_event_handler LinearSelector.clear_event_handlers + LinearSelector.format_pick_info LinearSelector.get_selected_data LinearSelector.get_selected_index LinearSelector.get_selected_indices + LinearSelector.map_model_to_world + LinearSelector.map_world_to_model LinearSelector.remove_event_handler LinearSelector.rotate diff --git a/docs/source/api/selectors/RectangleSelector.rst b/docs/source/api/selectors/RectangleSelector.rst index dc9727069..51f6801a4 100644 --- a/docs/source/api/selectors/RectangleSelector.rst +++ b/docs/source/api/selectors/RectangleSelector.rst @@ -35,8 +35,10 @@ Properties RectangleSelector.parent RectangleSelector.right_click_menu RectangleSelector.rotation + RectangleSelector.scale RectangleSelector.selection RectangleSelector.supported_events + RectangleSelector.tooltip_format RectangleSelector.vertex_color RectangleSelector.visible RectangleSelector.world_object @@ -49,9 +51,12 @@ Methods RectangleSelector.add_axes RectangleSelector.add_event_handler RectangleSelector.clear_event_handlers + RectangleSelector.format_pick_info RectangleSelector.get_selected_data RectangleSelector.get_selected_index RectangleSelector.get_selected_indices + RectangleSelector.map_model_to_world + RectangleSelector.map_world_to_model RectangleSelector.remove_event_handler RectangleSelector.rotate diff --git a/docs/source/api/tools/Cursor.rst b/docs/source/api/tools/Cursor.rst new file mode 100644 index 000000000..37a706d34 --- /dev/null +++ b/docs/source/api/tools/Cursor.rst @@ -0,0 +1,42 @@ +.. _api.Cursor: + +Cursor +****** + +====== +Cursor +====== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Cursor_api + + Cursor + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Cursor_api + + Cursor.alpha + Cursor.color + Cursor.edge_color + Cursor.edge_width + Cursor.enabled + Cursor.marker + Cursor.mode + Cursor.position + Cursor.size + Cursor.size_space + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Cursor_api + + Cursor.add_subplot + Cursor.clear + Cursor.remove_subplot + diff --git a/docs/source/api/tools/HistogramLUTTool.rst b/docs/source/api/tools/HistogramLUTTool.rst index 429f958e2..b3498dd68 100644 --- a/docs/source/api/tools/HistogramLUTTool.rst +++ b/docs/source/api/tools/HistogramLUTTool.rst @@ -32,7 +32,9 @@ Properties HistogramLUTTool.offset HistogramLUTTool.right_click_menu HistogramLUTTool.rotation + HistogramLUTTool.scale HistogramLUTTool.supported_events + HistogramLUTTool.tooltip_format HistogramLUTTool.visible HistogramLUTTool.vmax HistogramLUTTool.vmin @@ -46,6 +48,9 @@ Methods HistogramLUTTool.add_axes HistogramLUTTool.add_event_handler HistogramLUTTool.clear_event_handlers + HistogramLUTTool.format_pick_info + HistogramLUTTool.map_model_to_world + HistogramLUTTool.map_world_to_model HistogramLUTTool.remove_event_handler HistogramLUTTool.rotate HistogramLUTTool.set_data diff --git a/docs/source/api/tools/TextBox.rst b/docs/source/api/tools/TextBox.rst new file mode 100644 index 000000000..b202f4270 --- /dev/null +++ b/docs/source/api/tools/TextBox.rst @@ -0,0 +1,38 @@ +.. _api.TextBox: + +TextBox +******* + +======= +TextBox +======= +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextBox_api + + TextBox + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextBox_api + + TextBox.background_color + TextBox.font_size + TextBox.outline_color + TextBox.padding + TextBox.position + TextBox.text_color + TextBox.visible + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextBox_api + + TextBox.clear + TextBox.display + diff --git a/docs/source/api/tools/Tooltip.rst b/docs/source/api/tools/Tooltip.rst index 71607bf20..8e017370e 100644 --- a/docs/source/api/tools/Tooltip.rst +++ b/docs/source/api/tools/Tooltip.rst @@ -21,18 +21,20 @@ Properties :toctree: Tooltip_api Tooltip.background_color + Tooltip.continuous_update + Tooltip.enabled Tooltip.font_size Tooltip.outline_color Tooltip.padding + Tooltip.position Tooltip.text_color - Tooltip.world_object + Tooltip.visible Methods ~~~~~~~ .. autosummary:: :toctree: Tooltip_api - Tooltip.register - Tooltip.unregister - Tooltip.unregister_all + Tooltip.clear + Tooltip.display diff --git a/docs/source/api/tools/index.rst b/docs/source/api/tools/index.rst index c2666ed28..2bff8fb50 100644 --- a/docs/source/api/tools/index.rst +++ b/docs/source/api/tools/index.rst @@ -5,4 +5,6 @@ Tools :maxdepth: 1 HistogramLUTTool + TextBox Tooltip + Cursor diff --git a/docs/source/conf.py b/docs/source/conf.py index 8547e9ae7..edc172dad 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -68,6 +68,7 @@ "../../examples/text", "../../examples/events", "../../examples/selection_tools", + "../../examples/spaces_transforms", "../../examples/machine_learning", "../../examples/guis", "../../examples/ipywidgets", diff --git a/docs/source/user_guide/event_tables.rst b/docs/source/user_guide/event_tables.rst index ba53c3411..42f168bea 100644 --- a/docs/source/user_guide/event_tables.rst +++ b/docs/source/user_guide/event_tables.rst @@ -113,6 +113,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -378,6 +389,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -526,6 +548,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -751,6 +784,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -853,6 +897,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -996,6 +1051,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1124,6 +1190,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1252,6 +1329,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1387,6 +1475,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1541,6 +1640,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1695,6 +1805,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1794,6 +1915,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1895,6 +2027,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1996,6 +2139,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst index 8bf255507..bd0352aa7 100644 --- a/docs/source/user_guide/guide.rst +++ b/docs/source/user_guide/guide.rst @@ -648,23 +648,29 @@ There are several spaces to consider when using ``fastplotlib``: World space is the 3D space in which graphical objects live. Objects and the camera can exist anywhere in this space. -2) Data Space +2) Model or Data Space - Data space is simply the world space plus any offset or rotation that has been applied to an object. + Model/Data space is simply the world space plus any offset, scaling and rotation that has been applied to an object. .. note:: - World space does not always correspond directly to data space, you may have to adjust for any offset or rotation of the ``Graphic``. + World space does not always correspond directly to data space, + you may have to adjust for any offset, rotation, and scaling of the ``Graphic``. See below. 3) Screen Space Screen space is the 2D space in which your screen pixels reside. This space is constrained by the screen width and height in pixels. In the rendering process, the camera is responsible for projecting the world space into screen space. -.. note:: - When interacting with ``Graphic`` objects, there is a very helpful function for mapping screen space to world space - (``Figure.map_screen_to_world(pos=(x, y))``). This can be particularly useful when working with click events where click - positions are returned in screen space but ``Graphic`` objects that you may want to interact with exist in world - space. +When interacting with ``Graphic`` objects, there are helpful functions for mapping between these spaces: + - ``Subplot.map_screen_to_world((x, y))`` + - ``Subplot.map_world_to_screen((x, y, z))`` + - ``Graphic.map_model_to_world((x, y, z))`` + - ``Graphic.map_world_to_model((x, y, z))`` + +This can be particularly useful when working with click events where click positions are returned in screen space but + ``Graphic`` objects that you may want to interact with exist in world space. It can also be useful for determining + the screen/canvas pixel position of a datapoint on a graphic by mapping: model -> world -> screen. The entire inverse + transform can also be performed, screen -> world -> model. For more information on the various spaces used by rendering engines please see this `article `_ diff --git a/examples/line_collection/line_collection.py b/examples/line_collection/line_collection.py index 2ddfbe2ed..e3eea7392 100644 --- a/examples/line_collection/line_collection.py +++ b/examples/line_collection/line_collection.py @@ -29,7 +29,7 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: pos_xy = np.vstack(circles) -figure = fpl.Figure(size=(700, 560), show_tooltips=True) +figure = fpl.Figure(size=(700, 560)) figure[0, 0].add_line_collection(circles, cmap="jet", thickness=5) diff --git a/examples/line_collection/line_stack.py b/examples/line_collection/line_stack.py index 829708cb7..4376c18b4 100644 --- a/examples/line_collection/line_stack.py +++ b/examples/line_collection/line_stack.py @@ -21,7 +21,6 @@ figure = fpl.Figure( size=(700, 560), - show_tooltips=True ) line_stack = figure[0, 0].add_line_stack( @@ -32,25 +31,6 @@ ) -def tooltip_info(ev): - """A custom function to display the index of the graphic within the collection.""" - index = ev.pick_info["vertex_index"] # index of the line datapoint being hovered - - # get index of the hovered line within the line stack - line_index = np.where(line_stack.graphics == ev.graphic)[0].item() - info = f"line index: {line_index}\n" - - # append data value info - info += "\n".join(f"{dim}: {val}" for dim, val in zip("xyz", ev.graphic.data[index])) - - # return str to display in tooltip - return info - -# register the line stack with the custom tooltip function -figure.tooltip_manager.register( - line_stack, custom_info=tooltip_info -) - figure.show(maintain_aspect=False) diff --git a/examples/mesh/surface_ripple.py b/examples/mesh/surface_ripple.py index ac556bd1b..1adf676ea 100644 --- a/examples/mesh/surface_ripple.py +++ b/examples/mesh/surface_ripple.py @@ -34,6 +34,9 @@ def create_ripple(shape=(100, 100), phase=0.0, freq=np.pi / 4, ampl=1.0): z, mode="basic", cmap="viridis", clim=(-max_z, max_z) ) +# enable continuous updates for the tooltip +figure[0, 0].tooltip.continuous_update = True + figure[0, 0].camera.show_object(surface.world_object, (-1, 3, -1), up=(0, 0, 1)) figure.show() diff --git a/examples/misc/cursor_transform.py b/examples/misc/cursor_transform.py new file mode 100644 index 000000000..46478d8ce --- /dev/null +++ b/examples/misc/cursor_transform.py @@ -0,0 +1,54 @@ +""" +Cursor transform +================ + +Create a cursor and add them to subplots with a transform function. A common usecase is image registration. +""" + +# test_example = False +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + + +# get an image +img1 = iio.imread("imageio:camera.png") + +# create another image, but it is offset +img2 = np.zeros(img1.shape) +img2[50:, 20:] = img1[:-50, :-20] + +figure = fpl.Figure((1, 2), size=(700, 450)) + +# add images +figure[0, 0].add_image(img1) +figure[0, 1].add_image(img2) + +# create cursor +cursor = fpl.Cursor("crosshair") + +# add first subplot to cursor +cursor.add_subplot(figure[0, 0]) + +# a transform function for subplot 2 to indicate that the data is shifted +def transform_func(pos): + return (pos[0] + 20, pos[1] + 50) + +# add second subplot with a transform +cursor.add_subplot(figure[0, 1], transform=transform_func) + +figure.show() + +# you can programmatically set cursor position +cursor.position = (400, 120) + +# you can hide the canvas cursor, this is different and has nothing to do with the fastplotlib Cursor! +figure.canvas.set_cursor("none") + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/misc/cursors.py b/examples/misc/cursors.py new file mode 100644 index 000000000..030c254a4 --- /dev/null +++ b/examples/misc/cursors.py @@ -0,0 +1,48 @@ +""" +Cursor tool +=========== + +Example with multiple subplots and an interactive cursor that marks the same position in each subplot. +Default crosshair mode. +""" + +# test_example = False +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + + +# get some data +img1 = iio.imread("imageio:camera.png") +img2 = iio.imread("imageio:wikkie.png") +scatter_data = np.random.normal(loc=256, scale=(50), size=(500)).reshape(250, 2) +line_data = np.random.rand(100, 2) * 512 + +# create a figure +figure = fpl.Figure(shape=(2, 2), size=(700, 750)) + +# plot data +figure[0, 0].add_image(img1, cmap="viridis") +figure[0, 1].add_image(img2) +figure[1, 0].add_scatter(scatter_data, sizes=5, colors="r") +figure[1, 1].add_line(line_data, colors="r") + +# creator a cursor in crosshair mode +cursor = fpl.Cursor(color="w") + +# add all subplots to the cursor +for subplot in figure: + cursor.add_subplot(subplot) + +# you can also set the cursor position programmatically +cursor.position = (256, 256) + +figure.show() + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/misc/cursors_marker.py b/examples/misc/cursors_marker.py new file mode 100644 index 000000000..1b5437fe4 --- /dev/null +++ b/examples/misc/cursors_marker.py @@ -0,0 +1,47 @@ +""" +Cursor tool, marker mode +======================== + +Example with multiple subplots and an interactive cursor that marks the same position in each subplot. Marker mode. +""" + +# test_example = False +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + + +# get some data +img1 = iio.imread("imageio:camera.png") +img2 = iio.imread("imageio:wikkie.png") +scatter_data = np.random.normal(loc=256, scale=(50), size=(500)).reshape(250, 2) +line_data = np.random.rand(100, 2) * 512 + +# create a figure +figure = fpl.Figure(shape=(2, 2), size=(700, 750)) + +# plot data +figure[0, 0].add_image(img1, cmap="viridis") +figure[0, 1].add_image(img2) +figure[1, 0].add_scatter(scatter_data, sizes=5, colors="r") +figure[1, 1].add_line(line_data, colors="r") + +# creator a cursor in crosshair mode +cursor = fpl.Cursor(mode="marker", color="w", size=15) + +# add all subplots to the cursor +for subplot in figure: + cursor.add_subplot(subplot) + +# you can also set the cursor position programmatically +cursor.position = (256, 256) + +figure.show() + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/misc/tooltips.py b/examples/misc/tooltips.py deleted file mode 100644 index cad3d807c..000000000 --- a/examples/misc/tooltips.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Tooltips -======== - -Show tooltips on all graphics -""" - -# test_example = false -# sphinx_gallery_pygfx_docs = 'screenshot' - -import numpy as np -import imageio.v3 as iio -import fastplotlib as fpl - - -# get some data -scatter_data = np.random.rand(1_000, 3) - -xs = np.linspace(0, 2 * np.pi, 100) -ys = np.sin(xs) - -gray = iio.imread("imageio:camera.png") -rgb = iio.imread("imageio:astronaut.png") - -# create a figure -figure = fpl.Figure( - cameras=["3d", "2d", "2d", "2d"], - controller_types=["orbit", "panzoom", "panzoom", "panzoom"], - size=(700, 560), - shape=(2, 2), - show_tooltips=True, # tooltip will display data value info for all graphics -) - -# create graphics -scatter = figure[0, 0].add_scatter(scatter_data, sizes=3, colors="r") -line = figure[0, 1].add_line(np.column_stack([xs, ys])) -image = figure[1, 0].add_image(gray) -image_rgb = figure[1, 1].add_image(rgb) - - -figure.show() - -# to hide tooltips for all graphics in an existing Figure -# figure.show_tooltips = False - -# to show tooltips for all graphics in an existing Figure -# figure.show_tooltips = True - - -# NOTE: fpl.loop.run() should not be used for interactive sessions -# See the "JupyterLab and IPython" section in the user guide -if __name__ == "__main__": - print(__doc__) - fpl.loop.run() diff --git a/examples/misc/tooltips_custom.py b/examples/misc/tooltips_custom.py index d1cc1e297..3a54a945b 100644 --- a/examples/misc/tooltips_custom.py +++ b/examples/misc/tooltips_custom.py @@ -31,20 +31,26 @@ ) -def tooltip_info(ev) -> str: +def tooltip_info(pick_info: dict) -> str: # get index of the scatter point that is being hovered - index = ev.pick_info["vertex_index"] + index = pick_info["vertex_index"] # get the species name target = dataset["target"][index] cluster = agg.labels_[index] - info = f"species: {dataset['target_names'][target]}\ncluster: {cluster}" + + # the default formatting of the pick info + default_info = scatter.format_pick_info(pick_info) + + info = (f"species: {dataset['target_names'][target]}\n" + f"cluster: {cluster}\n\n" + f"{default_info}") # return this string to display it in the tooltip return info -figure.tooltip_manager.register(scatter, custom_info=tooltip_info) +scatter.tooltip_format = tooltip_info figure.show() diff --git a/examples/screenshots/no-imgui-rotation_image.png b/examples/screenshots/no-imgui-rotation_image.png new file mode 100644 index 000000000..3780dc87a --- /dev/null +++ b/examples/screenshots/no-imgui-rotation_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62b9923128bebb489e7da928c5d3fc212cc6228b58dbdaf4bcbaabf0ad12b28c +size 50262 diff --git a/examples/screenshots/no-imgui-rotation_line.png b/examples/screenshots/no-imgui-rotation_line.png new file mode 100644 index 000000000..3eddc6ff2 --- /dev/null +++ b/examples/screenshots/no-imgui-rotation_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c922741a05bc5ab2f6bf165b909bb14d443d93517700ceba522aa05b8aa26df4 +size 42402 diff --git a/examples/screenshots/no-imgui-scaling_image.png b/examples/screenshots/no-imgui-scaling_image.png new file mode 100644 index 000000000..5d3dbeaff --- /dev/null +++ b/examples/screenshots/no-imgui-scaling_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0481db08929abe0622f933b349746f40077fe930d86deed1a1ab08563ea310b +size 45587 diff --git a/examples/screenshots/no-imgui-scaling_line.png b/examples/screenshots/no-imgui-scaling_line.png new file mode 100644 index 000000000..8fd232e31 --- /dev/null +++ b/examples/screenshots/no-imgui-scaling_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71940e060068b1941f81e8aa66dfb9bae19aa60bd3c4ac848f65ecf42708dc85 +size 43106 diff --git a/examples/screenshots/no-imgui-translate_image.png b/examples/screenshots/no-imgui-translate_image.png new file mode 100644 index 000000000..a875ef91a --- /dev/null +++ b/examples/screenshots/no-imgui-translate_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0995cdaf81fc5a25ebdd54545b7be3e4edca6c25896c2aa5ba9d7e4ab0b240e8 +size 44246 diff --git a/examples/screenshots/no-imgui-translate_line.png b/examples/screenshots/no-imgui-translate_line.png new file mode 100644 index 000000000..211c4a5d0 --- /dev/null +++ b/examples/screenshots/no-imgui-translate_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8b3e79aeb1d8d0622e0928932bd98a7ee8a77d370dc7aecc7c1b923608497d7 +size 45889 diff --git a/examples/screenshots/no-imgui-translation_scaling_image.png b/examples/screenshots/no-imgui-translation_scaling_image.png new file mode 100644 index 000000000..a5c7a71d2 --- /dev/null +++ b/examples/screenshots/no-imgui-translation_scaling_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca48b15e42f7e5e2f67152a31e58b2869329d361d21b17718528b9f8f16a4c92 +size 45697 diff --git a/examples/screenshots/no-imgui-translation_scaling_line.png b/examples/screenshots/no-imgui-translation_scaling_line.png new file mode 100644 index 000000000..0c7b625c7 --- /dev/null +++ b/examples/screenshots/no-imgui-translation_scaling_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f2311cbd8a719d9c208d6744df56bba6d592f5e650cedc4c1251b7c5cf2c9b9 +size 42714 diff --git a/examples/screenshots/no-imgui-translation_scaling_rotation_image.png b/examples/screenshots/no-imgui-translation_scaling_rotation_image.png new file mode 100644 index 000000000..418ef1ff4 --- /dev/null +++ b/examples/screenshots/no-imgui-translation_scaling_rotation_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0035495345247d02c113c362699b930d11240e50c8bc14b4178457d029701629 +size 46978 diff --git a/examples/screenshots/no-imgui-translation_scaling_rotation_line.png b/examples/screenshots/no-imgui-translation_scaling_rotation_line.png new file mode 100644 index 000000000..15124c89e --- /dev/null +++ b/examples/screenshots/no-imgui-translation_scaling_rotation_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de3cac77e9f6601abf050b67fdd15f14e3fcfa691cc06284379830e9be57f3d4 +size 45515 diff --git a/examples/screenshots/rotation_image.png b/examples/screenshots/rotation_image.png new file mode 100644 index 000000000..85312949a --- /dev/null +++ b/examples/screenshots/rotation_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6399d67da50abbdf7af4430f2bc4264f893d239eb661d3664ead87563169bee +size 51598 diff --git a/examples/screenshots/rotation_line.png b/examples/screenshots/rotation_line.png new file mode 100644 index 000000000..08b09a417 --- /dev/null +++ b/examples/screenshots/rotation_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f66b0698d2f1fc2481767413377e21fa57bc80c9b34aa3e722a63902fc34a1e +size 44395 diff --git a/examples/screenshots/scaling_image.png b/examples/screenshots/scaling_image.png new file mode 100644 index 000000000..f0b2bdb8b --- /dev/null +++ b/examples/screenshots/scaling_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e820b72d87156d215f895c0668bef80a4a2d7cafeb1435a5df1ac7515d2336ef +size 47270 diff --git a/examples/screenshots/scaling_line.png b/examples/screenshots/scaling_line.png new file mode 100644 index 000000000..48e71b9ab --- /dev/null +++ b/examples/screenshots/scaling_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f611bbbf7c05b754a35065f7f2117fc8062f0024d209ae1fab049f6e7f2d3b8 +size 44380 diff --git a/examples/screenshots/translate_image.png b/examples/screenshots/translate_image.png new file mode 100644 index 000000000..c0e6dd76e --- /dev/null +++ b/examples/screenshots/translate_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c7fb592ea62eed3be0ff6c7650d176513304e455130b64caebcefc7e5fe48e9 +size 45572 diff --git a/examples/screenshots/translate_line.png b/examples/screenshots/translate_line.png new file mode 100644 index 000000000..4c64bbd74 --- /dev/null +++ b/examples/screenshots/translate_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a62c00847ea65187c7025c4eb0ad80767e1609e37d88602424531cbc0c7429a2 +size 46717 diff --git a/examples/screenshots/translation_scaling_image.png b/examples/screenshots/translation_scaling_image.png new file mode 100644 index 000000000..b7d26c937 --- /dev/null +++ b/examples/screenshots/translation_scaling_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:393d26c54bb9a0ac690411df262c3b9c3273274edf4787a18f057a1c3e02389e +size 47386 diff --git a/examples/screenshots/translation_scaling_line.png b/examples/screenshots/translation_scaling_line.png new file mode 100644 index 000000000..e3c6835b6 --- /dev/null +++ b/examples/screenshots/translation_scaling_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0593fa32a6990c2e05aad1b9314b912dc3e196b499938be49fc7074e610581e0 +size 44521 diff --git a/examples/screenshots/translation_scaling_rotation_image.png b/examples/screenshots/translation_scaling_rotation_image.png new file mode 100644 index 000000000..cd384ba15 --- /dev/null +++ b/examples/screenshots/translation_scaling_rotation_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42352d3bedbb42fdac5e45a789520e9f7be75748a32b12ceea7edabd4f17c500 +size 47418 diff --git a/examples/screenshots/translation_scaling_rotation_line.png b/examples/screenshots/translation_scaling_rotation_line.png new file mode 100644 index 000000000..ea92cdd09 --- /dev/null +++ b/examples/screenshots/translation_scaling_rotation_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25b9c03c40a1b5c91df269f402b953986d996a95660f0c5f4d85c8ef31d479a8 +size 46453 diff --git a/examples/spaces_transforms/README.rst b/examples/spaces_transforms/README.rst new file mode 100644 index 000000000..55747c2a8 --- /dev/null +++ b/examples/spaces_transforms/README.rst @@ -0,0 +1,2 @@ +Spaces and transforms +===================== diff --git a/examples/spaces_transforms/rotation_image.py b/examples/spaces_transforms/rotation_image.py new file mode 100644 index 000000000..ebc6cb3de --- /dev/null +++ b/examples/spaces_transforms/rotation_image.py @@ -0,0 +1,94 @@ +""" +Rotate image +============ + +This examples illustrates the various spaces that you may need to map between, +plots an image to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +# an image to demonstrate some data in model/data space +image_data = np.array( + [ + [0, 1, 2], + [3, 4, 5], + [5, 6, 7], + [8, 9, 10], + ] +) +image = figure[0, 0].add_image(image_data, cmap="turbo") + + +# a scatter that will be in the same space as the image +# used to indicates a few points on the image +scatter_data = np.array([[0, 1], [2, 3]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + +# rotation of pi/4 as a quaternion +rotation_quat = (np.cos(np.pi / 8), np.sin(np.pi / 8), 0, 0) +image.rotation = rotation_quat +scatter.rotation = rotation_quat + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/rotation_line.py b/examples/spaces_transforms/rotation_line.py new file mode 100644 index 000000000..bec820eb8 --- /dev/null +++ b/examples/spaces_transforms/rotation_line.py @@ -0,0 +1,89 @@ +""" +Rotate line +=========== + +This examples illustrates the various spaces that you may need to map between, +plots a line to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +# a line to demonstrate some data in model/data space +line_data = np.column_stack([xs, ys]) +line = figure[0, 0].add_line(line_data, cmap="jet", thickness=10) + +# a scatter that will be in the same space as the line +# used to indicates a few points on the line +scatter_data = np.array([[np.pi / 4, np.sin(np.pi / 4)], [3 * np.pi / 2 , -1]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + +# rotation of pi/4 as a quaternion +rotation_quat = (np.cos(np.pi / 8), np.sin(np.pi / 8), 0, 0) +line.rotation = rotation_quat +scatter.rotation = rotation_quat + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/scaling_image.py b/examples/spaces_transforms/scaling_image.py new file mode 100644 index 000000000..878a09010 --- /dev/null +++ b/examples/spaces_transforms/scaling_image.py @@ -0,0 +1,94 @@ +""" +Scale image +=========== + +This examples illustrates the various spaces that you may need to map between, +plots an image to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +# an image to demonstrate some data in model/data space +image_data = np.array( + [ + [0, 1, 2], + [3, 4, 5], + [5, 6, 7], + [8, 9, 10], + ] +) +image = figure[0, 0].add_image(image_data, cmap="turbo") + + +# a scatter that will be in the same space as the image +# used to indicates a few points on the image +scatter_data = np.array([[0, 1], [2, 3]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +image.scale = scaling +scatter.scale = scaling + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/scaling_line.py b/examples/spaces_transforms/scaling_line.py new file mode 100644 index 000000000..0fcdca55e --- /dev/null +++ b/examples/spaces_transforms/scaling_line.py @@ -0,0 +1,89 @@ +""" +Scale line +=========== + +This examples illustrates the various spaces that you may need to map between, +plots a line to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +# a line to demonstrate some data in model/data space +line_data = np.column_stack([xs, ys]) +line = figure[0, 0].add_line(line_data, cmap="jet", thickness=10) + +# a scatter that will be in the same space as the line +# used to indicates a few points on the line +scatter_data = np.array([[np.pi / 4, np.sin(np.pi / 4)], [3 * np.pi / 2 , -1]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +line.scale = scaling +scatter.scale = scaling + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translate_image.py b/examples/spaces_transforms/translate_image.py new file mode 100644 index 000000000..24a90a064 --- /dev/null +++ b/examples/spaces_transforms/translate_image.py @@ -0,0 +1,95 @@ +""" +Translate image +=============== + +This examples illustrates the various spaces that you may need to map between, +plots an image to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +# an image to demonstrate some data in model/data space +image_data = np.array( + [ + [0, 1, 2], + [3, 4, 5], + [5, 6, 7], + [8, 9, 10], + ] +) +image = figure[0, 0].add_image(image_data, cmap="turbo") + + +# a scatter that will be in the same space as the image +# used to indicates a few points on the image +scatter_data = np.array([[0, 1], [2, 3]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation +translation = (2, 3, 0) # x, y, z translation +image.offset = translation +scatter.offset = translation + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translate_line.py b/examples/spaces_transforms/translate_line.py new file mode 100644 index 000000000..d8821b271 --- /dev/null +++ b/examples/spaces_transforms/translate_line.py @@ -0,0 +1,90 @@ +""" +Translate line +============== + +This examples illustrates the various spaces that you may need to map between, +plots a line to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +# a line to demonstrate some data in model/data space +line_data = np.column_stack([xs, ys]) +line = figure[0, 0].add_line(line_data, cmap="jet", thickness=10) + +# a scatter that will be in the same space as the line +# used to indicates a few points on the line +scatter_data = np.array([[np.pi / 4, np.sin(np.pi / 4)], [3 * np.pi / 2 , -1]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation +translation = (2, 3, 0) # x, y, z translation +line.offset = translation +scatter.offset = translation + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translation_scaling_image.py b/examples/spaces_transforms/translation_scaling_image.py new file mode 100644 index 000000000..02e3a2d41 --- /dev/null +++ b/examples/spaces_transforms/translation_scaling_image.py @@ -0,0 +1,99 @@ +""" +Translate and scale image +========================= + +This examples illustrates the various spaces that you may need to map between, +plots an image to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +# an image to demonstrate some data in model/data space +image_data = np.array( + [ + [0, 1, 2], + [3, 4, 5], + [5, 6, 7], + [8, 9, 10], + ] +) +image = figure[0, 0].add_image(image_data, cmap="turbo") + + +# a scatter that will be in the same space as the image +# used to indicates a few points on the image +scatter_data = np.array([[0, 1], [2, 3]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation and scaling +translation = (2, 3, 0) # x, y, z translation +image.offset = translation +scatter.offset = translation + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +image.scale = scaling +scatter.scale = scaling + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translation_scaling_line.py b/examples/spaces_transforms/translation_scaling_line.py new file mode 100644 index 000000000..6afbfc11c --- /dev/null +++ b/examples/spaces_transforms/translation_scaling_line.py @@ -0,0 +1,94 @@ +""" +Translate and scale line +======================== + +This examples illustrates the various spaces that you may need to map between, +plots a line to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +# a line to demonstrate some data in model/data space +line_data = np.column_stack([xs, ys]) +line = figure[0, 0].add_line(line_data, cmap="jet", thickness=10) + +# a scatter that will be in the same space as the line +# used to indicates a few points on the line +scatter_data = np.array([[np.pi / 4, np.sin(np.pi / 4)], [3 * np.pi / 2 , -1]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation and scaling +translation = (2, 3, 0) # x, y, z translation +line.offset = translation +scatter.offset = translation + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +line.scale = scaling +scatter.scale = scaling + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translation_scaling_rotation_image.py b/examples/spaces_transforms/translation_scaling_rotation_image.py new file mode 100644 index 000000000..d0060401f --- /dev/null +++ b/examples/spaces_transforms/translation_scaling_rotation_image.py @@ -0,0 +1,102 @@ +""" +Translate scale and rotate image +================================ + +This examples illustrates the various spaces that you may need to map between, +plots an image to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +# an image to demonstrate some data in model/data space +image_data = np.array( + [ + [0, 1, 2], + [3, 4, 5], + [5, 6, 7], + [8, 9, 10], + ] +) +image = figure[0, 0].add_image(image_data, cmap="turbo") + + +# a scatter that will be in the same space as the image +# used to indicates a few points on the image +scatter_data = np.array([[0, 1], [2, 3]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation and scaling +translation = (2, 3, 0) # x, y, z translation +image.offset = translation +scatter.offset = translation + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +image.scale = scaling +scatter.scale = scaling + +# rotation of pi/4 as a quaternion +rotation_quat = (np.cos(np.pi / 8), np.sin(np.pi / 8), 0, 0) +image.rotation = rotation_quat +scatter.rotation = rotation_quat + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translation_scaling_rotation_line.py b/examples/spaces_transforms/translation_scaling_rotation_line.py new file mode 100644 index 000000000..e4c245a8e --- /dev/null +++ b/examples/spaces_transforms/translation_scaling_rotation_line.py @@ -0,0 +1,99 @@ +""" +Translate scale and rotate line +=============================== + +This examples illustrates the various spaces that you may need to map between, +plots a line to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +# a line to demonstrate some data in model/data space +line_data = np.column_stack([xs, ys]) +line = figure[0, 0].add_line(line_data, cmap="jet", thickness=10) + +# a scatter that will be in the same space as the line +# used to indicates a few points on the line +scatter_data = np.array([[np.pi / 4, np.sin(np.pi / 4)], [3 * np.pi / 2 , -1]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation and scaling +translation = (2, 3, 0) # x, y, z translation +line.offset = translation +scatter.offset = translation + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +line.scale = scaling +scatter.scale = scaling + +# rotation of pi/4 as a quaternion +rotation_quat = (np.cos(np.pi / 8), np.sin(np.pi / 8), 0, 0) +line.rotation = rotation_quat +scatter.rotation = rotation_quat + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index aad729c7a..e279809e3 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -30,6 +30,7 @@ "window_layouts/*.py", "events/*.py", "selection_tools/*.py", + "spaces_transforms/*.py", "misc/*.py", "guis/*.py", ] diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index a4f3e9a67..47673cbc0 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,6 +1,7 @@ +from __future__ import annotations from collections import defaultdict from functools import partial -from typing import Any, Literal, TypeAlias +from typing import Any, Literal, TypeAlias, Callable import weakref import numpy as np @@ -22,6 +23,7 @@ Name, Offset, Rotation, + Scale, Alpha, AlphaMode, Visible, @@ -29,11 +31,16 @@ from ._axes import Axes HexStr: TypeAlias = str +WorldObjectID: TypeAlias = int # dict that holds all world objects for a given python kernel/session # Graphic objects only use proxies to WorldObjects WORLD_OBJECTS: dict[HexStr, pygfx.WorldObject] = dict() #: {hex id str: WorldObject} +# maps world object to the graphic which owns it, useful when manually picking from the renderer and we +# need to know the graphic associated with the target world object +WORLD_OBJECT_TO_GRAPHIC: dict[WorldObjectID, Graphic] = dict() + PYGFX_EVENTS = [ "key_down", @@ -54,6 +61,11 @@ class Graphic: _features: dict[str, type] = dict() + # It also doesn't make sense to create tooltips for some graphics + # ex: text, that would be very funny. + # They would also get in the way of selector tools + _fpl_support_tooltip: bool = True + def __init_subclass__(cls, **kwargs): # set of all features @@ -62,6 +74,7 @@ def __init_subclass__(cls, **kwargs): "name": Name, "offset": Offset, "rotation": Rotation, + "scale": Scale, "alpha": Alpha, "alpha_mode": AlphaMode, "visible": Visible, @@ -72,8 +85,9 @@ def __init_subclass__(cls, **kwargs): def __init__( self, name: str = None, - offset: np.ndarray | list | tuple = (0.0, 0.0, 0.0), - rotation: np.ndarray | list | tuple = (0.0, 0.0, 0.0, 1.0), + offset: np.ndarray | tuple[float] = (0.0, 0.0, 0.0), + rotation: np.ndarray | tuple[float] = (0.0, 0.0, 0.0, 1.0), + scale: np.ndarray | tuple[float] = (1.0, 1.0, 1.0), alpha: float = 1.0, alpha_mode: str = "auto", visible: bool = True, @@ -92,6 +106,9 @@ def __init__( rotation: (float, float, float, float), default (0, 0, 0, 1) rotation quaternion + scale: (float, float, float), default (1.0, 1.0, 1.0) + (x, y, z) scale factors + alpha: (float), default 1.0 The global alpha value, i.e. opacity, of the graphic. @@ -155,6 +172,7 @@ def __init__( self._name = Name(name) self._deleted = Deleted(False) self._rotation = Rotation(rotation) + self._scale = Scale(scale) self._offset = Offset(offset) self._alpha = Alpha(alpha) self._alpha_mode = AlphaMode(alpha_mode) @@ -165,6 +183,11 @@ def __init__( self._right_click_menu = None + # store ids of all the WorldObjects that this Graphic manages/uses + self._world_object_ids = list() + + self._tooltip_format: Callable = None + @property def supported_events(self) -> tuple[str]: """events supported by this graphic""" @@ -185,7 +208,7 @@ def offset(self) -> np.ndarray: return self._offset.value @offset.setter - def offset(self, value: np.ndarray | list | tuple): + def offset(self, value: np.ndarray | tuple[float, float, float]): self._offset.set_value(self, value) @property @@ -194,9 +217,18 @@ def rotation(self) -> np.ndarray: return self._rotation.value @rotation.setter - def rotation(self, value: np.ndarray | list | tuple): + def rotation(self, value: np.ndarray | tuple[float, float, float, float]): self._rotation.set_value(self, value) + @property + def scale(self) -> np.ndarray: + """(x, y, z) scaling factor""" + return self._scale.value + + @scale.setter + def scale(self, value: np.ndarray | tuple[float, float, float]): + self._scale.set_value(self, value) + @property def alpha(self) -> float: """The opacity of the graphic""" @@ -251,6 +283,23 @@ def world_object(self) -> pygfx.WorldObject: def _set_world_object(self, wo: pygfx.WorldObject): WORLD_OBJECTS[self._fpl_address] = wo + # add to world object -> graphic mapping + if isinstance(wo, pygfx.Group): + for child in wo.children: + if isinstance( + child, (pygfx.Image, pygfx.Volume, pygfx.Points, pygfx.Line) + ): + # unique 32 bit integer id for each world object + global_id = child.id + WORLD_OBJECT_TO_GRAPHIC[global_id] = self + # store id to pop from dict when graphic is deleted + self._world_object_ids.append(global_id) + else: + global_id = wo.id + WORLD_OBJECT_TO_GRAPHIC[global_id] = self + # store id to pop from dict when graphic is deleted + self._world_object_ids.append(global_id) + wo.visible = self.visible if "Image" in self.__class__.__name__: # Image and ImageVolume use tiling and share one material @@ -269,6 +318,27 @@ def _set_world_object(self, wo: pygfx.WorldObject): if not all(wo.world.rotation == self.rotation): self.rotation = self.rotation + @property + def tooltip_format(self) -> Callable[[dict], str] | None: + """ + set a custom tooltip format function which takes a ``pick_info`` dict and + returns a str to be displayed in the tooltip + """ + return self._tooltip_format + + @tooltip_format.setter + def tooltip_format(self, func: Callable[[dict], str] | None): + if func is None: + self._tooltip_format = None + return + + if not callable(func): + raise TypeError( + f"`tooltip_format` must be set with a callable that takes a pick_info dict, or it can be set as None" + ) + + self._tooltip_format = func + @property def event_handlers(self) -> list[tuple[str, callable, ...]]: """ @@ -427,6 +497,72 @@ def my_handler(event): feature = getattr(self, f"_{t}") feature.remove_event_handler(wrapper) + def map_model_to_world( + self, position: tuple[float, float, float] | tuple[float, float] | np.ndarray + ) -> np.ndarray: + """ + map position from model (data) space to world space, basically applies the world affine transform + + Parameters + ---------- + position: (float, float, float) or (float, float) + (x, y, z) or (x, y) position. If z is not provided then the graphic's offset z is used. + + Returns + ------- + np.ndarray + (x, y, z) position in world space + + """ + + if len(position) == 2: + # use z of the graphic + position = [*position, self.offset[-1]] + + if len(position) != 3: + raise ValueError( + f"position must be tuple or array indicating (x, y, z) position in *model space*" + ) + + # apply world transform to project from model space to world space + return la.vec_transform(position, self.world_object.world.matrix) + + def map_world_to_model( + self, position: tuple[float, float, float] | tuple[float, float] | np.ndarray + ) -> np.ndarray: + """ + map position from world space to model (data) space, basically applies the inverse world affine transform + + Parameters + ---------- + position: (float, float, float) or (float, float) + (x, y, z) or (x, y) position. If z is not provided then 0 is used. + + Returns + ------- + np.ndarray + (x, y, z) position in world space + + """ + + if len(position) == 2: + # use z of the graphic + position = [*position, self.offset[-1]] + + if len(position) != 3: + raise ValueError( + f"position must be tuple or array indicating (x, y, z) position in *model space*" + ) + + return la.vec_transform(position, self.world_object.world.inverse_matrix) + + def format_pick_info(self, ev: pygfx.PointerEvent) -> str: + """ + Takes a pygfx.PointerEvent and returns formatted pick info. + """ + + raise NotImplementedError("must be implemented in subclass") + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area @@ -444,6 +580,10 @@ def _fpl_prepare_del(self): Optionally implemented in subclasses """ + # remove from world_obj -> graphic map + for global_id in self._world_object_ids: + WORLD_OBJECT_TO_GRAPHIC.pop(global_id) + # remove axes if added to this graphic if self._axes is not None: self._plot_area.scene.remove(self._axes) diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index 36f83ec7a..5b1fd87f1 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -181,6 +181,8 @@ class GraphicCollection(Graphic, CollectionProperties): _child_type: type _indexer: type + # tooltips will come from the child graphics + _fpl_support_tooltip = False def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) @@ -308,6 +310,7 @@ def _fpl_prepare_del(self): """ # clear any attached event handlers and animation functions self.world_object._event_handlers.clear() + self.world_object.clear() for g in self: g._fpl_prepare_del() @@ -318,16 +321,6 @@ def __getitem__(self, key) -> CollectionIndexer: return self._indexer(selection=self.graphics[key], features=self._features) - def __del__(self): - # detach children - self.world_object.clear() - - for g in self.graphics: - g._fpl_prepare_del() - del g - - super().__del__() - def __len__(self): return len(self._graphics) diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 73520cc84..af7d7badb 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -146,3 +146,11 @@ def __init__( self._size_space = SizeSpace(size_space) super().__init__(*args, **kwargs) + + def format_pick_info(self, pick_info: dict) -> str: + index = pick_info["vertex_index"] + info = "\n".join( + f"{dim}: {val:.4g}" for dim, val in zip("xyz", self.data[index]) + ) + + return info diff --git a/fastplotlib/graphics/_vectors.py b/fastplotlib/graphics/_vectors.py index 6f761bd49..be90db538 100644 --- a/fastplotlib/graphics/_vectors.py +++ b/fastplotlib/graphics/_vectors.py @@ -128,7 +128,7 @@ def __init__( } geometry = create_vector_geometry(color=color, **shape_options) - material = pygfx.MeshBasicMaterial() + material = pygfx.MeshBasicMaterial(pick_write=True) n_vectors = self._positions.value.shape[0] @@ -170,6 +170,16 @@ def directions(self) -> VectorDirections: def directions(self, new_directions): self._directions.set_value(self, new_directions) + def format_pick_info(self, pick_info: dict) -> str: + index = pick_info["instance_index"] + + info = ( + f"position: {self.positions[index]}\n" + f"direction: {self.directions[index]}" + ) + + return info + # mesh code copied and adapted from pygfx def generate_torso( diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py index cf99d376d..7f7410cf7 100644 --- a/fastplotlib/graphics/features/__init__.py +++ b/fastplotlib/graphics/features/__init__.py @@ -71,7 +71,7 @@ LinearRegionSelectionFeature, RectangleSelectionFeature, ) -from ._common import Name, Offset, Rotation, Alpha, AlphaMode, Visible, Deleted +from ._common import Name, Offset, Rotation, Scale, Alpha, AlphaMode, Visible, Deleted __all__ = [ @@ -119,6 +119,7 @@ "Name", "Offset", "Rotation", + "Scale", "Alpha", "AlphaMode", "Visible", diff --git a/fastplotlib/graphics/features/_common.py b/fastplotlib/graphics/features/_common.py index b2b99cc49..6ce167075 100644 --- a/fastplotlib/graphics/features/_common.py +++ b/fastplotlib/graphics/features/_common.py @@ -130,6 +130,55 @@ def set_value(self, graphic, value: np.ndarray | Sequence[float]): self._call_event_handlers(event) +class Scale(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray[float, float, float, float]", + "description": "new scale", + }, + ] + + def __init__( + self, value: np.ndarray | Sequence[float], property_name: str = "scale" + ): + """Graphic scaling factor""" + + self._validate(value) + # create ones array + self._value = np.ones(3) + + self._value[:] = value + super().__init__(property_name=property_name) + + def _validate(self, value): + if not len(value) in [2, 3]: + raise ValueError( + "scale must be a list, tuple, or array of 2 or 3 float values indicating (x, y) or (x, y, z) scaling" + ) + + @property + def value(self) -> np.ndarray: + return self._value + + @block_reentrance + def set_value(self, graphic, value: np.ndarray | Sequence[float]): + self._validate(value) + + if len(value) == 2: + value = (*value, graphic.world_object.world.scale_z) + + value = np.asarray(value) + + graphic.world_object.world.scale = value + + # set value of existing feature value array + self._value[:] = value + + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) + + class Alpha(GraphicFeature): """The alpha value (i.e. opacity) of a graphic.""" diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 1eaf54bb6..ece700385 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -21,6 +21,15 @@ ) +def _format_value(value: float): + """float -> rounded str, or str with scientific notation""" + abs_val = abs(value) + if abs_val < 0.01 or abs_val > 9_999: + return f"{value:.2e}" + else: + return f"{value:.4f}" + + class _ImageTile(pygfx.Image): """ Similar to pygfx.Image, only difference is that it modifies the pick_info @@ -477,3 +486,16 @@ def add_polygon_selector( self._plot_area.add_graphic(selector, center=False) return selector + + def format_pick_info(self, pick_info: dict) -> str: + col, row = pick_info["index"] + if self.data.value.ndim == 2: + val = self.data[row, col] + info = f"{val:.4g}" + else: + info = "\n".join( + f"{channel}: {val:.4g}" + for channel, val in zip("rgba", self.data[row, col]) + ) + + return info diff --git a/fastplotlib/graphics/image_volume.py b/fastplotlib/graphics/image_volume.py index db616b30d..db8f29eaa 100644 --- a/fastplotlib/graphics/image_volume.py +++ b/fastplotlib/graphics/image_volume.py @@ -419,3 +419,18 @@ def reset_vmin_vmax(self): vmin, vmax = quick_min_max(self.data.value) self.vmin = vmin self.vmax = vmax + + def format_pick_info(self, pick_info: dict) -> str: + return "image volume tooltips supported in next version" + + col, row, z = pick_info["index"] + if self.data.value.ndim == 3: + val = self.data[z, row, col] + info = f"{val:.4g}" + else: + info = "\n".join( + f"{channel}: {val:.4g}" + for channel, val in zip("rgba", self.data[z, row, col]) + ) + + return info diff --git a/fastplotlib/graphics/mesh.py b/fastplotlib/graphics/mesh.py index 2e5a11851..0e1ac42a3 100644 --- a/fastplotlib/graphics/mesh.py +++ b/fastplotlib/graphics/mesh.py @@ -291,6 +291,27 @@ def plane(self, value: tuple[float, float, float, float]): self._plane.set_value(self, value) + def format_pick_info(self, pick_info: dict) -> str: + # Get what face was clicked + face_index = pick_info["face_index"] + coords = pick_info["face_coord"] + # Select which of the three vertices was closest + # Note that you can also select all vertices for this face, + # or use the coords to select the closest edge. + sub_index = np.argmax(coords) + # Look up the vertex index + try: + vertex_index = int(self.indices[face_index, sub_index]) + except IndexError: + # if vertex buffer sizes change then the pointer event can have outdated pick info? + return "error, buffer size changed" + + info = "\n".join( + f"{dim}: {val:.4g}" for dim, val in zip("xyz", self.positions[vertex_index]) + ) + + return info + class SurfaceGraphic(MeshGraphic): _features = { diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index e4dbc890b..28c6534a7 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -40,6 +40,8 @@ class MoveInfo: # Selector base class class BaseSelector(Graphic): + _fpl_support_tooltip = False + @property def axis(self) -> str: return self._axis diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 9f1aeb8af..37e559576 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -21,6 +21,8 @@ class TextGraphic(Graphic): "outline_thickness": TextOutlineThickness, } + _fpl_support_tooltip = False + def __init__( self, text: str, diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 8fd5dc666..79b5be3a8 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -21,7 +21,6 @@ from ._subplot import Subplot from ._engine import GridLayout, WindowLayout, ScreenSpaceCamera from .. import ImageGraphic -from ..tools import Tooltip class Figure: @@ -52,7 +51,6 @@ def __init__( canvas_kwargs: dict = None, size: tuple[int, int] = (500, 300), names: list | np.ndarray = None, - show_tooltips: bool = False, ): """ Create a Figure containing Subplots. @@ -124,10 +122,30 @@ def __init__( names: list or array of str, optional subplot names - show_tooltips: bool, default False - show tooltips on graphics - """ + # create canvas and renderer + if canvas_kwargs is not None: + if size not in canvas_kwargs.keys(): + canvas_kwargs["size"] = size + else: + canvas_kwargs = {"size": size, "max_fps": 60.0, "vsync": True} + + canvas, renderer = make_canvas_and_renderer( + canvas, renderer, canvas_kwargs=canvas_kwargs + ) + + canvas.add_event_handler(self._fpl_reset_layout, "resize") + + self._canvas = canvas + self._renderer = renderer + + # underlay render pass + self._underlay_camera = ScreenSpaceCamera() + self._underlay_scene = pygfx.Scene() + + # overlay render pass + self._overlay_camera = ScreenSpaceCamera() + self._fpl_overlay_scene = pygfx.Scene() if rects is not None: if not all(isinstance(v, (np.ndarray, tuple, list)) for v in rects): @@ -202,18 +220,6 @@ def __init__( else: subplot_names = None - if canvas_kwargs is not None: - if size not in canvas_kwargs.keys(): - canvas_kwargs["size"] = size - else: - canvas_kwargs = {"size": size, "max_fps": 60.0, "vsync": True} - - canvas, renderer = make_canvas_and_renderer( - canvas, renderer, canvas_kwargs=canvas_kwargs - ) - - 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 cameras = np.array([cameras] * n_subplots) @@ -392,9 +398,6 @@ def __init__( for cam in cams[1:]: _controller.add_camera(cam) - self._canvas = canvas - self._renderer = renderer - if layout_mode == "grid": n_rows, n_cols = shape grid_index_iterator = list(product(range(n_rows), range(n_cols))) @@ -449,23 +452,10 @@ def __init__( canvas_rect=self.get_pygfx_render_area(), ) - # underlay render pass - self._underlay_camera = ScreenSpaceCamera() - self._underlay_scene = pygfx.Scene() - + # add subplot frames to underlay for subplot in self._subplots.ravel(): self._underlay_scene.add(subplot.frame._world_object) - # overlay render pass - self._overlay_camera = ScreenSpaceCamera() - self._overlay_scene = pygfx.Scene() - - # tooltip in overlay render pass - self._tooltip_manager = Tooltip() - self._overlay_scene.add(self._tooltip_manager.world_object) - - self._show_tooltips = show_tooltips - self._animate_funcs_pre: list[callable] = list() self._animate_funcs_post: list[callable] = list() @@ -533,34 +523,11 @@ def names(self) -> np.ndarray[str]: names.flags.writeable = False return names - @property - def tooltip_manager(self) -> Tooltip: - """manage tooltips""" - return self._tooltip_manager - - @property - def show_tooltips(self) -> bool: - """show/hide tooltips for all graphics""" - return self._show_tooltips - @property def animations(self) -> dict[str, list[callable]]: """Returns a dictionary of 'pre' and 'post' animation functions.""" return {"pre": self._animate_funcs_pre, "post": self._animate_funcs_post} - @show_tooltips.setter - def show_tooltips(self, val: bool): - self._show_tooltips = val - - if val: - # register all graphics - for subplot in self: - for graphic in subplot.graphics: - self._tooltip_manager.register(graphic) - - elif not val: - self._tooltip_manager.unregister_all() - def _render(self, draw=True): # draw the underlay planes self.renderer.render(self._underlay_scene, self._underlay_camera, flush=False) @@ -578,7 +545,7 @@ def _render(self, draw=True): # overlay render pass if hasattr(self.renderer, "clear"): self.renderer.clear(depth=True) - self.renderer.render(self._overlay_scene, self._overlay_camera, flush=False) + self.renderer.render(self._fpl_overlay_scene, self._overlay_camera, flush=False) self.renderer.flush() diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index 046c622ea..d54be4086 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -44,7 +44,6 @@ def __init__( canvas_kwargs: dict = None, size: tuple[int, int] = (500, 300), names: list | np.ndarray = None, - show_tooltips: bool = False, ): self._guis: dict[str, EdgeWindow] = {k: None for k in GUI_EDGES} @@ -61,7 +60,6 @@ def __init__( canvas_kwargs=canvas_kwargs, size=size, names=names, - show_tooltips=show_tooltips, ) self._imgui_renderer = ImguiRenderer(self.renderer.device, self.canvas) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 01721780c..f83dcfbcb 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -9,11 +9,12 @@ from rendercanvas import BaseRenderCanvas from ._utils import create_controller -from ..graphics._base import Graphic +from ..graphics._base import Graphic, WORLD_OBJECT_TO_GRAPHIC from ..graphics import ImageGraphic from ..graphics.selectors._base_selector import BaseSelector from ._graphic_methods_mixin import GraphicMethodsMixin from ..legends import Legend +from ..tools import Tooltip try: @@ -88,6 +89,8 @@ def __init__( self._animate_funcs_pre: list[callable] = list() self._animate_funcs_post: list[callable] = list() + self._animate_funcs_persist: list[callable] = list() + # list of all graphics managed by this PlotArea self._graphics: list[Graphic] = list() @@ -123,6 +126,10 @@ def __init__( self.scene.add(self._ambient_light) self.scene.add(self._camera.add(self._directional_light)) + self._tooltip = Tooltip() + self.get_figure()._fpl_overlay_scene.add(self._tooltip._fpl_world_object) + self.renderer.add_event_handler(self._fpl_set_tooltip, "pointer_move") + def get_figure(self, obj=None): """Get Figure instance that contains this plot area""" if obj is None: @@ -297,17 +304,27 @@ def animations(self) -> dict[str, list[callable]]: """Returns a dictionary of 'pre' and 'post' animation functions.""" return {"pre": self._animate_funcs_pre, "post": self._animate_funcs_post} + @property + def tooltip(self) -> Tooltip: + """The tooltip in this PlotArea""" + return self._tooltip + 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 + Map screen (canvas) position to world position Parameters ---------- pos: (float, float) | pygfx.PointerEvent ``(x, y)`` screen coordinates, or ``pygfx.PointerEvent`` + Returns + ------- + (float, float, float) + (x, y, z) position in world space, z is always 0 + """ if isinstance(pos, pygfx.PointerEvent): pos = pos.x, pos.y @@ -333,6 +350,117 @@ def map_screen_to_world( # default z is zero for now return np.array([*pos_world[:2], 0]) + def map_world_to_screen( + self, pos: tuple[float, float, float] | np.ndarray + ) -> tuple[float, float]: + """ + Map world position to screen (canvas) position + + Parameters + ---------- + pos: (x, y, z) + world space position + + Returns + ------- + (float, float) + (x, y) position in screen (canvas) space + + """ + + if not len(pos) == 3: + raise ValueError(f"must pass 3d (x, y, z) position, you passed: {pos}") + + # apply camera transform and get NDC position + ndc = vec_transform(np.asarray(pos), self.camera.camera_matrix) + + # get viewport rect + x_offset, y_offset, w, h = self.viewport.rect + + # ndc to screen position + x_screen = x_offset + (ndc[0] + 1) * 0.5 * w + y_screen = y_offset + (1 - ndc[1]) * 0.5 * h + + return x_screen, y_screen + + def get_pick_info(self, pos): + """ + Get pick info at this screen position + + Parameters + ---------- + pos: (x, y) + screen space position + + Returns + ------- + dict | None + pick info if a graphic is at this position, else None + + """ + + info = self.renderer.get_pick_info(pos) + + if info["world_object"] is not None: + # if this world object is owned by a graphic + if info["world_object"].id in WORLD_OBJECT_TO_GRAPHIC.keys(): + info["graphic"] = WORLD_OBJECT_TO_GRAPHIC[info["world_object"].id] + return info + + def _fpl_set_tooltip(self, ev: pygfx.PointerEvent): + # set tooltip using pointer position + if not self._tooltip.enabled: + return + + # is pointer in this plot area + if not self.viewport.is_inside(ev.x, ev.y): + return + + # is there a world object under the pointer + if ev.target is not None: + # is it owned by a graphic + if ev.target.id in WORLD_OBJECT_TO_GRAPHIC.keys(): + graphic = WORLD_OBJECT_TO_GRAPHIC[ev.target.id] + if not graphic._fpl_support_tooltip: + return + + pick_info = ev.pick_info + if graphic.tooltip_format is not None: + # custom formatter + info = graphic.tooltip_format(pick_info) + else: + # default formatter for this graphic + info = graphic.format_pick_info(pick_info) + self._tooltip.display((ev.x, ev.y), info) + return + + # not over a graphic that supports tooltips + self._tooltip.clear() + + def _fpl_update_tooltip_render(self): + # update tooltip on every render + # TODO: improve performance + if (not self._tooltip.visible) or (not self._tooltip.enabled): + return + + pick_info = self.get_pick_info(self._tooltip.position) + + # None if no graphic is at this position + if pick_info is not None: + graphic = pick_info["graphic"] + if graphic._fpl_support_tooltip: + if graphic.tooltip_format is not None: + # custom formatter + info = graphic.tooltip_format(pick_info) + else: + # default formatter for this graphic + info = graphic.format_pick_info(pick_info) + self._tooltip.display(self._tooltip.position, info) + return + + # tooltip cleared if none of the above condiitionals reached the tooltip display call + self._tooltip.clear() + def _render(self): self._call_animate_functions(self._animate_funcs_pre) @@ -344,6 +472,9 @@ def _render(self): self._call_animate_functions(self._animate_funcs_post) + if self._tooltip.continuous_update: + self._fpl_update_tooltip_render() + def _call_animate_functions(self, funcs: list[callable]): for fn in funcs: try: @@ -565,10 +696,6 @@ def _add_or_insert_graphic( obj_list = self._graphics self._fpl_graphics_scene.add(graphic.world_object) - # add to tooltip registry - if self.get_figure().show_tooltips: - self.get_figure().tooltip_manager.register(graphic) - else: raise TypeError("graphic must be of type Graphic | BaseSelector | Legend") diff --git a/fastplotlib/tools/__init__.py b/fastplotlib/tools/__init__.py index df129a369..761183f76 100644 --- a/fastplotlib/tools/__init__.py +++ b/fastplotlib/tools/__init__.py @@ -1,7 +1,10 @@ from ._histogram_lut import HistogramLUTTool -from ._tooltip import Tooltip +from ._textbox import TextBox, Tooltip +from ._cursor import Cursor __all__ = [ "HistogramLUTTool", + "TextBox", "Tooltip", + "Cursor", ] diff --git a/fastplotlib/tools/_cursor.py b/fastplotlib/tools/_cursor.py new file mode 100644 index 000000000..6b7946cd0 --- /dev/null +++ b/fastplotlib/tools/_cursor.py @@ -0,0 +1,420 @@ +from functools import partial +from typing import Literal, Sequence, Callable + +import numpy as np +import pygfx + +from ..layouts import Subplot +from ..utils import RenderQueue + + +class Cursor: + def __init__( + self, + mode: Literal["crosshair", "marker"] = "crosshair", + size: float = 1.0, # in screen space + color: str | Sequence[float] | pygfx.Color | np.ndarray = "w", + marker: str = "+", + edge_color: str | Sequence[float] | pygfx.Color | np.ndarray = "k", + edge_width: float = 0.5, + alpha: float = 0.7, + size_space: Literal["screen", "world"] = "screen", + ): + """ + A cursor that indicates the same position in world-space across subplots. + + Parameters + ---------- + mode: "crosshair" | "marker" + cursor mode + + size: float, default 1.0 + * if ``mode`` == 'crosshair', this is the crosshair line thickness + * if ``mode`` == 'marker', it's the size of the marker + + You probably want to use ``size > 5`` if ``mode`` is 'marker' and ``size_space`` is ``screen`` + + color: str | Sequence[float] | pygfx.Color | np.ndarray, default "r" + color of the marker + + marker: str, default "+" + marker shape, used if mode == 'marker' + + edge_color: str | Sequence[float] | pygfx.Color | np.ndarray, default "k" + marker edge color, used if ``mode`` == 'marker' + + edge_width: float, default 0.5 + marker edge widget, used if ``mode`` == 'marker' + + alpha: float, default 0.7 + alpha (transparency) of the cursor + + size_space: "screen" | "world", default "screen" + size space of the cursor, if "screen" the ``size`` is exact screen pixels. + if "world" the ``size`` is in world-space + + """ + + self._cursors: dict[Subplot, pygfx.Points | pygfx.Group[pygfx.Line]] = dict() + self._transforms: dict[Subplot, Callable | None] = dict() + + self._mode = None + self.mode = mode + self.size = size + self.color = color + self.marker = marker + self.edge_color = edge_color + self.edge_width = edge_width + self.alpha = alpha + self.size_space = size_space + + self._enabled = True + + self._position: list[float, float] = [0.0, 0.0] + + @property + def mode(self) -> Literal["crosshair", "marker"]: + """cursor mode, one of 'crosshair' or 'marker'""" + return self._mode + + @mode.setter + def mode(self, mode: Literal["crosshair", "marker"]): + if not (mode == "crosshair" or mode == "marker"): + raise ValueError( + f"mode must be one of: 'crosshair' | 'marker', you passed: {mode}" + ) + + if mode == self.mode: + return + + # mode has changed, clear and create new world objects + subplots = list(self._cursors.keys()) + + self.clear() + + for subplot in subplots: + self.add_subplot(subplot) + + self._mode = mode + + @property + def size(self) -> float: + """size of marker or crosshair line thickness""" + return self._size + + @size.setter + def size(self, new_size: float): + for c in self._cursors.values(): + if self.mode == "marker": + c.material.size = new_size + elif self.mode == "crosshair": + h, v = c.children + h.material.thickness = new_size + v.material.thickness = new_size + + self._size = new_size + + @property + def size_space(self) -> Literal["screen", "world"]: + """interpret cursor size in screen or world space""" + return self._size_space + + @size_space.setter + def size_space(self, space: Literal["screen", "world"]): + if space not in ["screen", "world", "model"]: + raise ValueError( + f"valid `size_space` is one of: 'screen' | 'world'. You passed: {space}" + ) + + for c in self._cursors.values(): + if self.mode == "marker": + c.material.size_space = space + + elif self.mode == "crosshair": + h, v = c.children + h.material.thickness_space = space + v.material.thickness_space = space + + self._size_space = space + + @property + def color(self) -> pygfx.Color: + """cursor color""" + return self._color + + @color.setter + def color(self, new_color): + new_color = pygfx.Color(new_color) + + for c in self._cursors.values(): + c.material.color = new_color + + self._color = new_color + + @property + def marker(self) -> str: + """cursor marker shape, if `mode` is 'marker'""" + return self._marker + + @marker.setter + def marker(self, new_marker: str): + if self.mode == "marker": + for c in self._cursors.values(): + c.material.marker = new_marker + + self._marker = new_marker + + @property + def edge_color(self) -> pygfx.Color: + """cursor marker edge color, if `mode` is 'marker'""" + return self._edge_color + + @edge_color.setter + def edge_color(self, new_color: str | Sequence | np.ndarray | pygfx.Color): + new_color = pygfx.Color(new_color) + + if self.mode == "marker": + for c in self._cursors.values(): + c.material.edge_color = new_color + + self._edge_color = new_color + + @property + def edge_width(self) -> float: + """cursor marker edge width, if `mode` is 'marker'""" + return self._edge_width + + @edge_width.setter + def edge_width(self, new_width: float): + if self.mode == "marker": + for c in self._cursors.values(): + c.material.edge_width = new_width + + self._edge_width = new_width + + @property + def alpha(self) -> float: + """cursor alpha value""" + return self._alpha + + @alpha.setter + def alpha(self, value: float): + for c in self._cursors.values(): + c.material.opacity = value + + self._alpha = value + + @property + def enabled(self) -> bool: + """enable/disable the cursor, if False the cursor will not respond to mouse pointer events""" + return self._enabled + + @enabled.setter + def enabled(self, pause: bool): + self._enabled = bool(pause) + + @property + def position(self) -> tuple[float, float]: + """(x, y) position in world space""" + return tuple(self._position) + + @position.setter + def position(self, pos: tuple[float, float]): + for subplot, cursor in self._cursors.items(): + if self._transforms[subplot] is not None: + pos_transformed = self._transforms[subplot](pos) + else: + pos_transformed = pos + + if self.mode == "marker": + cursor.geometry.positions.data[0, :-1] = pos_transformed + cursor.geometry.positions.update_full() + + elif self.mode == "crosshair": + line_h, line_v = cursor.children + + # set x vals for horizontal line + line_h.geometry.positions.data[0, 0] = pos_transformed[0] - 1 + line_h.geometry.positions.data[1, 0] = pos[0] + 1 + + # set y value + line_h.geometry.positions.data[:, 1] = pos_transformed[1] + + line_h.geometry.positions.update_full() + + # set y vals for vertical line + line_v.geometry.positions.data[0, 1] = pos_transformed[1] - 1 + line_v.geometry.positions.data[1, 1] = pos_transformed[1] + 1 + + # set x value + line_v.geometry.positions.data[:, 0] = pos_transformed[0] + + line_v.geometry.positions.update_full() + + # set tooltip using pick info if a graphic is at this position + # for now we just set z = 1 + screen_pos = subplot.map_world_to_screen((*pos_transformed, 1)) + pick_info = subplot.get_pick_info(screen_pos) + + self._position[:] = pos_transformed + + if pick_info is not None: + graphic = pick_info["graphic"] + if ( + graphic._fpl_support_tooltip + ): # some graphics don't support tooltips, ex: Text + if graphic.tooltip_format is not None: + # custom formatter + info = graphic.tooltip_format + else: + # default formatter for this graphic + info = graphic.format_pick_info(pick_info) + + subplot.tooltip.display(screen_pos, info) + continue + + # tooltip cleared if none of the above condiitionals reached the tooltip display call + subplot.tooltip.clear() + + def add_subplot(self, subplot: Subplot, transform: Callable | None = None): + """ + Add a subplot to this cursor, with an optional position transform function + + Parameters + ---------- + subplot: Subplot + subplot to add + + transform: Callable[[tuple[float, float]], tuple[float, float]] | None + a transform function that takes the cursor's position and returns a transformed + position at which the cursor will visually appear. + + """ + if subplot in self._cursors.keys(): + raise KeyError(f"The given subplot has already been added to this cursor") + + if (not callable(transform)) and (transform is not None): + raise TypeError( + f"`transform` must be a callable or `None`, you passed: {transform}" + ) + + if self.mode == "marker": + cursor = self._create_marker() + + elif self.mode == "crosshair": + cursor = self._create_crosshair() + + subplot.scene.add(cursor) + subplot.renderer.add_event_handler( + partial(self._pointer_moved, subplot), "pointer_move" + ) + + self._cursors[subplot] = cursor + self._transforms[subplot] = transform + + # let cursor manage tooltips + subplot.renderer.remove_event_handler(subplot._fpl_set_tooltip, "pointer_move") + + def remove_subplot(self, subplot: Subplot): + """remove a subplot""" + if subplot not in self._cursors.keys(): + raise KeyError("cursor not in given supblot") + + subplot.scene.remove(self._cursors.pop(subplot)) + + # give back tooltip control to the subplot + subplot.renderer.add_event_handler(subplot._fpl_set_tooltip, "pointer_move") + + def clear(self): + """remove all subplots""" + for subplot in self._cursors.keys(): + self.remove_subplot(subplot) + + def _create_marker(self) -> pygfx.Points: + # creates a Point object, used for "marker" mode + point = pygfx.Points( + pygfx.Geometry(positions=np.array([[*self.position, 0]], dtype=np.float32)), + pygfx.PointsMarkerMaterial( + marker=self.marker, + size=self.size, + size_space=self.size_space, + color=self.color, + edge_color=self.edge_color, + edge_width=self.edge_width, + opacity=self.alpha, + alpha_mode="blend", + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=False, + ), + ) + + return point + + def _create_crosshair(self) -> pygfx.Group: + # Creates two infinite lines, used for "crosshair" mode + x, y = self.position + line_h_data = np.array( + [ + [x - 1, y, 0], + [x + 1, y, 0], + ], + dtype=np.float32, + ) + + line_v_data = np.array( + [ + [x, y - 1, 0], + [x, y + 1, 0], + ], + dtype=np.float32, + ) + + line_h = pygfx.Line( + geometry=pygfx.Geometry(positions=line_h_data), + material=pygfx.LineInfiniteSegmentMaterial( + thickness=self.size, + thickness_space=self.size_space, + color=self.color, + opacity=self.alpha, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=False, + ), + ) + + line_v = pygfx.Line( + geometry=pygfx.Geometry(positions=line_v_data), + material=pygfx.LineInfiniteSegmentMaterial( + thickness=self.size, + thickness_space=self.size_space, + color=self.color, + opacity=self.alpha, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=False, + ), + ) + + lines = pygfx.Group() + lines.add(line_h, line_v) + + return lines + + def _pointer_moved(self, subplot, ev: pygfx.PointerEvent): + if not self.enabled: + return + + pos = subplot.map_screen_to_world(ev) + + if pos is None: + return + + self.position = pos[:-1] diff --git a/fastplotlib/tools/_histogram_lut.py b/fastplotlib/tools/_histogram_lut.py index 7507a7ff2..d651137da 100644 --- a/fastplotlib/tools/_histogram_lut.py +++ b/fastplotlib/tools/_histogram_lut.py @@ -27,6 +27,8 @@ def _get_image_graphic_events(image_graphic: ImageGraphic) -> list[str]: # TODO: This is a widget, we can think about a BaseWidget class later if necessary class HistogramLUTTool(Graphic): + _fpl_support_tooltip = False + def __init__( self, data: np.ndarray, diff --git a/fastplotlib/tools/_tooltip.py b/fastplotlib/tools/_textbox.py similarity index 55% rename from fastplotlib/tools/_tooltip.py rename to fastplotlib/tools/_textbox.py index f6c9cf531..46a468ae7 100644 --- a/fastplotlib/tools/_tooltip.py +++ b/fastplotlib/tools/_textbox.py @@ -1,11 +1,7 @@ -from functools import partial - import numpy as np import pygfx from ..utils.enums import RenderQueue -from ..graphics import LineGraphic, ImageGraphic, ScatterGraphic, Graphic -from ..graphics.features import GraphicFeatureEvent class MeshMasks: @@ -51,21 +47,48 @@ class MeshMasks: masks = MeshMasks -class Tooltip: - def __init__(self): +class TextBox: + def __init__( + self, + font_size: int = 12, + text_color: str | pygfx.Color | tuple = "w", + background_color: str | pygfx.Color | tuple = (0.1, 0.1, 0.3, 0.95), + outline_color: str | pygfx.Color | tuple = (0.8, 0.8, 1.0, 1.0), + padding: tuple[float, float] = (5, 5), + ): + """ + Create a Textbox + + Parameters + ---------- + font_size: int, default 12 + text font size + + text_color: str | pygfx.Color | tuple, default "w" + text color, interpretable by pygfx.Color + + background_color: str | pygfx.Color | tuple, default (0.1, 0.1, 0.3, 0.95), + background color, interpretable by pygfx.Color + + outline_color: str | pygfx.Color | tuple, default (0.8, 0.8, 1.0, 1.0) + outline color, interpretable by pygfx.Color + + padding: (float, float), default (5, 5) + the amount of pixels in (x, y) by which to extend the rectangle behind the text + + """ + # text object self._text = pygfx.Text( text="", - font_size=12, - screen_space=False, + font_size=font_size, + screen_space=False, # these are added to the overlay render pass so it will actually be in screen space! anchor="bottom-left", material=pygfx.TextMaterial( alpha_mode="blend", aa=True, render_queue=RenderQueue.overlay, - color="w", - outline_color="w", - outline_thickness=0.0, + color=text_color, depth_write=False, depth_test=False, pick_write=False, @@ -77,7 +100,7 @@ def __init__(self): material = pygfx.MeshBasicMaterial( alpha_mode="blend", render_queue=RenderQueue.overlay, - color=(0.1, 0.1, 0.3, 0.95), + color=background_color, depth_write=False, depth_test=False, ) @@ -101,7 +124,7 @@ def __init__(self): alpha_mode="blend", render_queue=RenderQueue.overlay, thickness=1.0, - color=(0.8, 0.8, 1.0, 1.0), + color=outline_color, depth_write=False, depth_test=False, ), @@ -109,18 +132,21 @@ def __init__(self): # Plane gets rendered before text and line self._plane.render_order = -1 - self._world_object = pygfx.Group() - self._world_object.add(self._plane, self._text, self._line) + self._fpl_world_object = pygfx.Group() + self._fpl_world_object.add(self._plane, self._text, self._line) # padded to bbox so the background box behind the text extends a bit further # making the text easier to read - self._padding = np.array([[5, 5, 0], [-5, -5, 0]], dtype=np.float32) + self._padding = np.zeros(shape=(2, 3), dtype=np.float32) + self.padding = padding - self._registered_graphics = dict() + # position of the tooltip in screen space + self._position = np.array([0.0, 0.0]) @property - def world_object(self) -> pygfx.Group: - return self._world_object + def position(self) -> np.ndarray: + """position of the tooltip in screen space""" + return self._position @property def font_size(self): @@ -172,9 +198,37 @@ def padding(self, padding_xy: tuple[float, float]): self._padding[0, :2] = padding_xy self._padding[1, :2] = -np.asarray(padding_xy) - def _set_position(self, pos: tuple[float, float]): + @property + def visible(self) -> bool: + """get or set the visibility""" + return self._fpl_world_object.visible + + @visible.setter + def visible(self, visible: bool): + self._fpl_world_object.visible = visible + + def display(self, position: tuple[float, float], info: str): + """ + display at the given position in screen space + + Parameters + ---------- + position: (x, y) + position in screen space + + info: str + tooltip text to display + + """ + # set the text and top left position of the tooltip + self.visible = True + self._text.set_text(info) + self._draw_tooltip(position) + self._position[:] = position + + def _draw_tooltip(self, pos: tuple[float, float]): """ - Set the position of the tooltip + Sets the positions of the world objects so it's draw at the given position Parameters ---------- @@ -182,6 +236,9 @@ def _set_position(self, pos: tuple[float, float]): position in screen space """ + if np.array_equal(self.position, pos): + return + # need to flip due to inverted y x, y = pos[0], pos[1] @@ -207,110 +264,36 @@ def _set_position(self, pos: tuple[float, float]): self._line.geometry.positions.data[:, :2] = pts self._line.geometry.positions.update_range() - def _event_handler(self, custom_tooltip: callable, ev: pygfx.PointerEvent): - """Handles the tooltip appear event, determines the text to be set in the tooltip""" - if custom_tooltip is not None: - info = custom_tooltip(ev) - - elif isinstance(ev.graphic, ImageGraphic): - col, row = ev.pick_info["index"] - if ev.graphic.data.value.ndim == 2: - info = str(ev.graphic.data[row, col]) - else: - info = "\n".join( - f"{channel}: {val}" - for channel, val in zip("rgba", ev.graphic.data[row, col]) - ) - - elif isinstance(ev.graphic, (LineGraphic, ScatterGraphic)): - index = ev.pick_info["vertex_index"] - info = "\n".join( - f"{dim}: {val}" for dim, val in zip("xyz", ev.graphic.data[index]) - ) - else: - raise TypeError("Unsupported graphic") - - # make the tooltip object visible - self.world_object.visible = True - - # set the text and top left position of the tooltip - self._text.set_text(info) - self._set_position((ev.x, ev.y)) - - def _clear(self, ev): + def clear(self, *args): + """clear the text box and make it invisible""" self._text.set_text("") - self.world_object.visible = False - - def register( - self, - graphic: Graphic, - appear_event: str = "pointer_move", - disappear_event: str = "pointer_leave", - custom_info: callable = None, - ): - """ - Register a Graphic to display tooltips. - - **Note:** if the passed graphic is already registered then it first unregistered - and then re-registered using the given arguments. - - Parameters - ---------- - graphic: Graphic - Graphic to register - - appear_event: str, default "pointer_move" - the pointer that triggers the tooltip to appear. Usually one of "pointer_move" | "click" | "double_click" - - disappear_event: str, default "pointer_leave" - the event that triggers the tooltip to disappear, does not have to be a pointer event. + self._fpl_world_object.visible = False - custom_info: callable, default None - a custom function that takes the pointer event defined as the `appear_event` and returns the text - to display in the tooltip - """ - if graphic in list(self._registered_graphics.keys()): - # unregister first and then re-register - self.unregister(graphic) - - pfunc = partial(self._event_handler, custom_info) - graphic.add_event_handler(pfunc, appear_event) - graphic.add_event_handler(self._clear, disappear_event) - - self._registered_graphics[graphic] = (pfunc, appear_event, disappear_event) - - # automatically unregister when graphic is deleted - graphic.add_event_handler(self.unregister, "deleted") - - def unregister(self, graphic: Graphic): - """ - Unregister a Graphic to no longer display tooltips for this graphic. - - **Note:** if the passed graphic is not registered then it is just ignored without raising any exception. - - Parameters - ---------- - graphic: Graphic - Graphic to unregister - - """ +class Tooltip(TextBox): + def __init__(self): + super().__init__() + self._enabled: bool = True + self._continuous_update = False + self.visible = False - if isinstance(graphic, GraphicFeatureEvent): - # this happens when the deleted event is triggered - graphic = graphic.graphic + @property + def enabled(self) -> bool: + """enable or disable the tooltip""" + return self._enabled - if graphic not in self._registered_graphics: - return + @enabled.setter + def enabled(self, value: bool): + self._enabled = bool(value) - # get pfunc and event names - pfunc, appear_event, disappear_event = self._registered_graphics.pop(graphic) + if not self.enabled: + self.visible = False - # remove handlers from graphic - graphic.remove_event_handler(pfunc, appear_event) - graphic.remove_event_handler(self._clear, disappear_event) + @property + def continuous_update(self) -> bool: + """update the tooltip on every render""" + return self._continuous_update - def unregister_all(self): - """unregister all graphics""" - for graphic in self._registered_graphics.keys(): - self.unregister(graphic) + @continuous_update.setter + def continuous_update(self, value: bool): + self._continuous_update = bool(value)