Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
sudo apt install -y libegl1-mesa-dev libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers

- name: Install dependencies
run: uv sync --no-dev --group test --extra ${{ matrix.gfx }}
run: uv sync --no-dev --group test --extra ${{ matrix.gfx }} --extra imgui

- name: Test
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion examples/basic_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
view.scene.add_child(image)

# both are optional, just for example
snx.use("pygfx")
# snx.use("pygfx")
# snx.use("vispy")

snx.show(view)
Expand Down
3 changes: 2 additions & 1 deletion src/scenex/adaptors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
in the model.
"""

from ._auto import get_adaptor_registry, run, use
from ._auto import get_adaptor_registry, get_all_adaptors, run, use
from ._base import Adaptor
from ._registry import AdaptorRegistry

__all__ = [
"Adaptor",
"AdaptorRegistry",
"get_adaptor_registry",
"get_all_adaptors",
"run",
"use",
]
17 changes: 16 additions & 1 deletion src/scenex/adaptors/_auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

import importlib.util
import os
from typing import TYPE_CHECKING, Literal, TypeAlias, TypeGuard, get_args
import sys
from contextlib import suppress
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypeGuard, cast, get_args

if TYPE_CHECKING:
from collections.abc import Iterator

from scenex.adaptors._base import Adaptor

from ._registry import AdaptorRegistry

KnownBackend: TypeAlias = Literal["vispy", "pygfx"]
Expand Down Expand Up @@ -34,6 +40,15 @@ def get_adaptor_registry(backend: KnownBackend | str | None = None) -> AdaptorRe
return _pygfx.adaptors


def get_all_adaptors(obj: Any) -> Iterator[Adaptor]:
"""Get all adaptors for the given object."""
for mod_name in ["scenex.adaptors._vispy", "scenex.adaptors._pygfx"]:
if mod := sys.modules.get(mod_name):
reg = cast("AdaptorRegistry", mod.adaptors)
with suppress(KeyError):
yield reg.get_adaptor(obj, create=False)


def determine_backend(request: KnownBackend | str | None = None) -> KnownBackend:
"""Get the name of the backend adaptor registry.

Expand Down
11 changes: 10 additions & 1 deletion src/scenex/adaptors/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
TNode = TypeVar("TNode", bound="model.Node", covariant=True)
TCamera = TypeVar("TCamera", bound="model.Camera", covariant=True)
TImage = TypeVar("TImage", bound="model.Image", covariant=True)
TVolume = TypeVar("TVolume", bound="model.Volume", covariant=True)
TPoints = TypeVar("TPoints", bound="model.Points", covariant=True)
TCanvas = TypeVar("TCanvas", bound="model.Canvas", covariant=True)
TView = TypeVar("TView", bound="model.View", covariant=True)
Expand Down Expand Up @@ -47,6 +48,9 @@ def _snx_get_native(self) -> TNative:
def handle_event(self, info: EmissionInfo) -> None:
"""Receive info from psygnal callback and convert to adaptor call."""
signal_name = info.signal.name
if signal_name == "parent":
# Parent change events are handled by the parent adaptor.
return

try:
name = self.SETTER_METHOD.format(name=signal_name)
Expand Down Expand Up @@ -138,6 +142,11 @@ def _snx_set_gamma(self, arg: float, /) -> None: ...
def _snx_set_interpolation(self, arg: model.InterpolationMode, /) -> None: ...


class VolumeAdaptor(ImageAdaptor[TVolume, TNative]):
@abstractmethod
def _snx_set_render_mode(self, arg: model.RenderMode, /) -> None: ...


class PointsAdaptor(NodeAdaptor[TPoints, TNative]):
"""Protocol for a backend Image adaptor object."""

Expand All @@ -154,7 +163,7 @@ def _snx_set_edge_width(self, arg: float, /) -> None: ...
@abstractmethod
def _snx_set_symbol(self, arg: str, /) -> None: ...
@abstractmethod
def _snx_set_scaling(self, arg: str, /) -> None: ...
def _snx_set_scaling(self, arg: model.ScalingMode, /) -> None: ...
@abstractmethod
def _snx_set_antialias(self, arg: float, /) -> None: ...

Expand Down
4 changes: 3 additions & 1 deletion src/scenex/adaptors/_pygfx/_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import numpy as np
import pygfx

from scenex.adaptors._base import ImageAdaptor

from ._node import Node

if TYPE_CHECKING:
Expand All @@ -17,7 +19,7 @@
logger = logging.getLogger("scenex.adaptors.pygfx")


class Image(Node):
class Image(Node, ImageAdaptor):
"""pygfx backend adaptor for an Image node."""

_pygfx_node: pygfx.Image
Expand Down
4 changes: 3 additions & 1 deletion src/scenex/adaptors/_pygfx/_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import numpy as np
import pygfx

from scenex.adaptors._base import PointsAdaptor

from ._node import Node

if TYPE_CHECKING:
Expand All @@ -25,7 +27,7 @@
}


class Points(Node):
class Points(Node, PointsAdaptor):
"""Vispy backend adaptor for an Points node."""

_pygfx_node: pygfx.Points
Expand Down
8 changes: 5 additions & 3 deletions src/scenex/adaptors/_pygfx/_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import numpy as np
import pygfx

from scenex.adaptors._base import VolumeAdaptor

from ._node import Node

if TYPE_CHECKING:
Expand All @@ -15,7 +17,7 @@
from scenex import model


class Volume(Node):
class Volume(Node, VolumeAdaptor):
"""pygfx backend adaptor for a Volume node."""

_pygfx_node: pygfx.Volume
Expand All @@ -24,7 +26,7 @@ class Volume(Node):

def __init__(self, volume: model.Volume, **backend_kwargs: Any) -> None:
self._snx_set_data(volume.data)
self._snx_set_rendermode(volume.render_mode, volume.interpolation)
self._snx_set_render_mode(volume.render_mode, volume.interpolation)
self._pygfx_node = pygfx.Volume(self._geometry, self._material)

def _snx_set_cmap(self, arg: Colormap) -> None:
Expand Down Expand Up @@ -55,7 +57,7 @@ def _snx_set_data(self, data: ArrayLike) -> None:
self._texture = self._create_texture(np.asanyarray(data))
self._geometry = pygfx.Geometry(grid=self._texture)

def _snx_set_rendermode(
def _snx_set_render_mode(
self,
data: model.RenderMode,
interpolation: model.InterpolationMode | None = None,
Expand Down
5 changes: 4 additions & 1 deletion src/scenex/adaptors/_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@
"""Get the adaptor for the given model object, create if `create` is True."""
if obj._model_id.hex not in self._objects:
if not create:
raise KeyError(f"No adaptor found for {obj!r}, and create=False")
raise KeyError(

Check warning on line 92 in src/scenex/adaptors/_registry.py

View check run for this annotation

Codecov / codecov/patch

src/scenex/adaptors/_registry.py#L92

Added line #L92 was not covered by tests
f"{type(self).__name__!r} has no adaptor for {type(obj)} @ "
f"{id(obj):x}, and create=False"
)
logger.debug(
"Creating %r Adaptor %-14r id: %s",
type(self).__module__,
Expand Down
4 changes: 3 additions & 1 deletion src/scenex/adaptors/_vispy/_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import vispy.scene
import vispy.visuals

from scenex.adaptors._base import ImageAdaptor

from ._node import Node

if TYPE_CHECKING:
Expand All @@ -14,7 +16,7 @@
from scenex import model


class Image(Node):
class Image(Node, ImageAdaptor):
"""pygfx backend adaptor for an Image node."""

_vispy_node: vispy.visuals.ImageVisual
Expand Down
6 changes: 4 additions & 2 deletions src/scenex/adaptors/_vispy/_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import vispy.scene
import vispy.visuals

from scenex.adaptors._base import PointsAdaptor

from ._node import Node

if TYPE_CHECKING:
Expand All @@ -26,7 +28,7 @@
}


class Points(Node):
class Points(Node, PointsAdaptor):
"""Vispy backend adaptor for an Points node."""

_model: model.Points
Expand Down Expand Up @@ -63,7 +65,7 @@ def _snx_set_edge_width(self, edge_width: float) -> None:
def _snx_set_symbol(self, symbol: str) -> None:
self._update_vispy_data()

def _snx_set_scaling(self, scaling: str) -> None:
def _snx_set_scaling(self, scaling: model.ScalingMode) -> None:
self._update_vispy_data()

def _snx_set_antialias(self, antialias: float) -> None:
Expand Down
6 changes: 4 additions & 2 deletions src/scenex/adaptors/_vispy/_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import vispy.scene
import vispy.visuals

from scenex.adaptors._base import VolumeAdaptor

from ._node import Node

if TYPE_CHECKING:
Expand All @@ -15,7 +17,7 @@
from scenex import model


class Volume(Node):
class Volume(Node, VolumeAdaptor):
"""vispy backend adaptor for a Volume node."""

_vispy_node: vispy.visuals.VolumeVisual
Expand All @@ -41,7 +43,7 @@ def _snx_set_interpolation(self, arg: model.InterpolationMode) -> None:
def _snx_set_data(self, data: ArrayLike) -> None:
self._vispy_node.set_data(np.asarray(data))

def _snx_set_rendermode(
def _snx_set_render_mode(
self,
data: model.RenderMode,
interpolation: model.InterpolationMode | None = None,
Expand Down
4 changes: 2 additions & 2 deletions src/scenex/imgui/_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
def add_imgui_controls(view: View) -> None:
"""Add imgui controls to the given canvas."""
snx_canvas_model = view.canvas
snx_canvas_adaptor = snx_canvas_model._get_adaptor()
snx_view_adaptor = view._get_adaptor()
snx_canvas_adaptor = snx_canvas_model._get_adaptors(backend="pygfx")[0]
snx_view_adaptor = view._get_adaptors(backend="pygfx")[0]
render_canv = snx_canvas_model._get_native()

if not (
Expand Down
17 changes: 10 additions & 7 deletions src/scenex/model/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,19 @@ def __repr_args__(self) -> Iterable[tuple[str | None, Any]]:
continue
yield key, val

def _get_adaptor(
def _get_adaptors(
self, backend: str | None = None, create: bool = False
) -> "Adaptor":
) -> list["Adaptor"]:
"""Get all adaptors for this model."""
from scenex.adaptors import get_adaptor_registry
from scenex.adaptors import get_adaptor_registry, get_all_adaptors

reg = get_adaptor_registry(backend=backend)
return reg.get_adaptor(self, create=create)
if backend:
reg = get_adaptor_registry(backend=backend)
return [reg.get_adaptor(self, create=create)]
else:
return list(get_all_adaptors(self))

def _get_native(self, backend: str | None = None, create: bool = False) -> Any:
"""Get the native object for this model."""
adaptor = self._get_adaptor(backend=backend, create=create)
return adaptor._snx_get_native()
if adaptors := self._get_adaptors(backend=backend, create=create):
return adaptors[0]._snx_get_native()
5 changes: 3 additions & 2 deletions src/scenex/model/_canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@

def render(self) -> np.ndarray:
"""Show the canvas."""
adaptor = cast("CanvasAdaptor", self._get_adaptor())
return adaptor._snx_render()
if adaptors := self._get_adaptors():
return cast("CanvasAdaptor", adaptors[0])._snx_render()
raise RuntimeError("No adaptor found for Canvas.")

Check warning on line 56 in src/scenex/model/_canvas.py

View check run for this annotation

Codecov / codecov/patch

src/scenex/model/_canvas.py#L54-L56

Added lines #L54 - L56 were not covered by tests
7 changes: 4 additions & 3 deletions src/scenex/model/_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
self._canvas.views.append(self)

def render(self) -> np.ndarray:
"""Show the canvas."""
adaptor = cast("ViewAdaptor", self._get_adaptor())
return adaptor._snx_render()
"""Render the view to an array."""
if adaptors := self._get_adaptors():
return cast("ViewAdaptor", adaptors[0])._snx_render()
raise RuntimeError("No adaptor found for View.")

Check warning on line 81 in src/scenex/model/_view.py

View check run for this annotation

Codecov / codecov/patch

src/scenex/model/_view.py#L81

Added line #L81 was not covered by tests
10 changes: 8 additions & 2 deletions src/scenex/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,23 @@ def _ensure_iterable(obj: object) -> Iterable[Any]:
)


def show(obj: model.Node | model.View | model.Canvas) -> model.Canvas:
def show(
obj: model.Node | model.View | model.Canvas, *, backend: str | None = None
) -> model.Canvas:
"""Show a scene or view.

Parameters
----------
obj : Node | View | Canvas
The scene or view to show. If a Node is provided, it will be wrapped in a Scene
and then in a View.
backend : str, optional
The backend to use for rendering. If not specified, the default backend will be
used. Defaults to None.
"""
from .adaptors import get_adaptor_registry

view = None
if isinstance(obj, model.Canvas):
canvas = obj
else:
Expand All @@ -115,7 +121,7 @@ def show(obj: model.Node | model.View | model.Canvas) -> model.Canvas:
canvas = model.Canvas(views=[view]) # pyright: ignore[reportArgumentType]

canvas.visible = True
reg = get_adaptor_registry()
reg = get_adaptor_registry(backend=backend)
reg.get_adaptor(canvas, create=True)
for view in canvas.views:
cam = reg.get_adaptor(view.camera)
Expand Down
13 changes: 10 additions & 3 deletions tests/test_basic_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,15 @@ def _child_names(obj: Any) -> list[str]:
return [_obj_name(child) for child in obj.children]


def test_basic_view(basic_view: snx.View) -> None:
snx.show(basic_view)
@pytest.mark.parametrize("backend", BACKENDS)
def test_basic_view(basic_view: snx.View, backend: str) -> None:
snx.show(basic_view, backend=backend)

# make sure we've got the right backend
adaptors = basic_view._get_adaptors(backend=backend, create=False)
assert adaptors
assert backend in type(adaptors[0]).__module__

assert isinstance(repr(basic_view), str)
assert isinstance(basic_view.model_dump(), dict)
assert isinstance(basic_view.model_dump_json(), str)
Expand All @@ -58,7 +65,7 @@ def test_basic_view(basic_view: snx.View) -> None:
def test_view_tree_matches_native(basic_view: snx.View, backend: str) -> None:
"""Test that the structure of the tree generated by the model matches the
structure of the tree generated by the native backend."""
basic_view._get_adaptor(backend=backend, create=True)
basic_view._get_adaptors(backend=backend, create=True)

model_tree = snx.util.tree_dict(basic_view.scene, obj_name=_obj_name)
native_scene = basic_view.scene._get_native(backend=backend)
Expand Down
Loading
Loading