Skip to content

scenex.adaptors#

scenex.adaptors #

Backend adaptors that translate scenex models into graphics library calls.

Adaptors bridge the gap between scenex's declarative models and rendering backends (e.g. pygfx). For each model class in scenex.model, there should be a corresponding adaptor class for each backend that handles the actual GPU rendering.

When you call scenex.show(), adaptors are automatically created for each object in your scene graph. The adaptors translate model properties (colors, transforms, data) into backend-specific commands. As you modify the models, events trigger updates in the adaptors to keep the rendered scene synced.

Architecture

The adaptor system uses a registry pattern::

Model (declarative) → Adaptor (imperative) → Backend (GPU library)

Image model         → ImageAdaptor         → pygfx.Mesh + texture
                    or
                    → ImageAdaptor         → vispy.scene.Image

Each backend (pygfx, vispy) has its own set of adaptors implementing the same model-to-native translation logic tailored to that backend's API.

Main Components
  • AdaptorRegistry: Maps model classes to adaptor classes
  • Adaptor: Base class for all adaptors, handles event subscription
  • Backend-specific adaptors: In _pygfx and _vispy subpackages
Supported Backends

pygfx (WebGPU-based): - Modern GPU API with advanced rendering features vispy (OpenGL-based): - Mature, widely-supported OpenGL renderer

See Also

scenex.model : Declarative model classes scenex.use : Function to select rendering backend scenex.show : Function that creates adaptors

Classes:

Functions:

  • get_adaptor_registry

    Get the backend adaptor registry.

  • get_all_adaptors

    Get all loaded adaptors for the given object.

  • use

    Set the graphics backend for rendering scenex visualizations.

Adaptor #

Adaptor(obj: TModel)

Bases: ABC, Generic[TModel, TNative]


              flowchart TD
              scenex.adaptors.Adaptor[Adaptor]

              

              click scenex.adaptors.Adaptor href "" "scenex.adaptors.Adaptor"
            

ABC for backend adaptor classes.

An adaptor converts model change events into into native calls for the given backend.

All backend adaptor objects receive the object they are adapting.

Methods:

  • handle_event

    Receive info from psygnal callback and convert to adaptor call.

Source code in src/scenex/adaptors/_base.py
43
44
45
@abstractmethod
def __init__(self, obj: TModel) -> None:
    """All backend adaptor objects receive the object they are adapting."""

handle_event #

handle_event(info: EmissionInfo) -> None

Receive info from psygnal callback and convert to adaptor call.

Source code in src/scenex/adaptors/_base.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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)
        setter = getattr(self, name)
    except AttributeError as e:
        logger.exception(e)
        return

    arg = info.args[0]
    logger.debug("EVENT: %r -> %s=%r  ", type(self), signal_name, arg)

    try:
        setter(arg)
    except Exception as e:
        logger.exception(e)

AdaptorRegistry #

AdaptorRegistry()

Weak registry for Adaptor objects.

Each backend should subclass this and implement the get_adaptor_class method. And expose an instance of the subclass as adaptors in the top level of the backend module.

Methods:

  • all

    Return an iterator over all adaptors in the registry.

  • create_adaptor

    Create a new adaptor for the given model object.

  • get_adaptor

    Get the adaptor for the given model object, create if create is True.

  • get_adaptor_class

    Return the adaptor class for the given model object.

  • initialize_adaptor

    Initialize the adaptor for the given model object.

  • validate_adaptor_class

    Validate that the given class is a valid adaptor for the given object.

Source code in src/scenex/adaptors/_registry.py
33
34
def __init__(self) -> None:
    self._objects: dict[str, _base.Adaptor] = {}

all #

all() -> Iterator[Adaptor]

Return an iterator over all adaptors in the registry.

Source code in src/scenex/adaptors/_registry.py
36
37
38
def all(self) -> Iterator[_base.Adaptor]:
    """Return an iterator over all adaptors in the registry."""
    yield from self._objects.values()

create_adaptor #

create_adaptor(model: _M) -> Adaptor[_M, Any]

Create a new adaptor for the given model object.

Source code in src/scenex/adaptors/_registry.py
143
144
145
146
147
148
149
def create_adaptor(self, model: _M) -> _base.Adaptor[_M, Any]:
    """Create a new adaptor for the given model object."""
    adaptor_cls: type[_base.Adaptor] = self.get_adaptor_class(model)
    self.validate_adaptor_class(model, adaptor_cls)
    adaptor = adaptor_cls(model)

    return adaptor

get_adaptor #

get_adaptor(obj: Points, create: bool = ...) -> PointsAdaptor
get_adaptor(obj: Image, create: bool = ...) -> ImageAdaptor
get_adaptor(obj: Camera, create: bool = ...) -> CameraAdaptor
get_adaptor(obj: Scene, create: bool = ...) -> NodeAdaptor
get_adaptor(obj: View, create: bool = ...) -> ViewAdaptor
get_adaptor(obj: Canvas, create: bool = ...) -> CanvasAdaptor
get_adaptor(obj: EventedBase, create: bool = ...) -> Adaptor
get_adaptor(obj: _M, create: bool = True) -> Adaptor[_M, Any]

Get the adaptor for the given model object, create if create is True.

Source code in src/scenex/adaptors/_registry.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def get_adaptor(self, obj: _M, create: bool = True) -> _base.Adaptor[_M, Any]:
    """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"{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__,
            type(obj).__name__,
            obj._model_id.hex[:8],
        )
        self._objects[obj._model_id.hex] = adaptor = self.create_adaptor(obj)
        self.initialize_adaptor(obj, adaptor)
    return self._objects[obj._model_id.hex]

get_adaptor_class #

get_adaptor_class(obj: EventedBase) -> type[Adaptor]

Return the adaptor class for the given model object.

Source code in src/scenex/adaptors/_registry.py
127
128
129
130
131
132
133
134
def get_adaptor_class(self, obj: model.EventedBase) -> type[_base.Adaptor]:
    """Return the adaptor class for the given model object."""
    cls = type(self)
    cls_module = sys.modules[cls.__module__]
    cls_file = cls_module.__file__
    raise NotImplementedError(
        f"{cls.__name__}.get_adaptor_class not implemented in {cls_file}"
    )

initialize_adaptor #

initialize_adaptor(model: EventedBase, adaptor: Adaptor) -> None

Initialize the adaptor for the given model object.

Source code in src/scenex/adaptors/_registry.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def initialize_adaptor(
    self, model: model.EventedBase, adaptor: _base.Adaptor
) -> None:
    """Initialize the adaptor for the given model object."""
    # syncronize all model properties with the adaptor
    sync_adaptor(adaptor, model)
    # connect the model events to the adaptor, to keep the adaptor in sync

    model.events.connect(adaptor.handle_event)

    if isinstance(model, models.Canvas):
        for view in model.views:
            self.get_adaptor(view, create=True)
    if isinstance(model, models.View):
        self.get_adaptor(model.scene, create=True)
    if isinstance(model, models.Node):
        adaptor = cast("_base.NodeAdaptor", adaptor)
        model.child_added.connect(adaptor._snx_add_child)
        model.child_removed.connect(adaptor._snx_remove_child)
        for child in model.children:
            # perhaps optional ... since _implementations of _snx_add_child
            # will also likely need to call get_adaptor
            self.get_adaptor(child, create=True)
            adaptor._snx_add_child(child)

validate_adaptor_class classmethod #

validate_adaptor_class(obj: EventedBase, adaptor_cls: type[Adaptor]) -> None

Validate that the given class is a valid adaptor for the given object.

Source code in src/scenex/adaptors/_registry.py
136
137
138
139
140
141
@classmethod
def validate_adaptor_class(
    cls, obj: model.EventedBase, adaptor_cls: type[_base.Adaptor]
) -> None:
    """Validate that the given class is a valid adaptor for the given object."""
    return _validate_adaptor_class(type(obj), adaptor_cls)

get_adaptor_registry #

get_adaptor_registry(backend: KnownBackend | str | None = None) -> AdaptorRegistry

Get the backend adaptor registry.

Source code in src/scenex/adaptors/_auto.py
30
31
32
33
34
35
36
37
38
39
40
def get_adaptor_registry(backend: KnownBackend | str | None = None) -> AdaptorRegistry:
    """Get the backend adaptor registry."""
    match determine_backend(backend):
        case "vispy":
            from . import _vispy

            return _vispy.adaptors
        case "pygfx":
            from . import _pygfx

            return _pygfx.adaptors

get_all_adaptors #

get_all_adaptors(obj: Any) -> Iterator[Adaptor]

Get all loaded adaptors for the given object.

Source code in src/scenex/adaptors/_auto.py
43
44
45
46
47
48
49
def get_all_adaptors(obj: Any) -> Iterator[Adaptor]:
    """Get all loaded 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)

use #

use(backend: KnownBackend | None = None) -> None

Set the graphics backend for rendering scenex visualizations.

This function allows you to explicitly select which graphics library (backend) scenex should use for rendering. It is the goal of scenex to support the full range of model API for each backend.

If not called, scenex will automatically select an arbitrary available backend. You can also set the backend via the SCENEX_CANVAS_BACKEND environment variable.

Parameters:

  • backend #

    (Literal['pygfx', 'vispy'] | None, default: None ) –

    The graphics backend to use: - "pygfx": Modern WebGPU-based renderer with advanced features - "vispy": OpenGL-based renderer with broad compatibility - None: Reset to auto-detection

Raises:

  • ValueError

    If the specified backend is not one of the known backends.

Examples:

Use pygfx backend explicitly:

>>> import scenex as snx
>>> snx.use("pygfx")
>>> canvas = snx.show(snx.View())

Use vispy backend:

>>> snx.use("vispy")
>>> canvas = snx.show(snx.Scene())

Reset to auto-detection:

>>> snx.use(None)
Notes

The backend selection follows this priority order: 1. Backend specified by this function 2. SCENEX_CANVAS_BACKEND environment variable 3. Auto-detection (pygfx preferred, then vispy)

This function should be called before creating any visualizations.

Source code in src/scenex/adaptors/_auto.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def use(backend: KnownBackend | None = None) -> None:
    """Set the graphics backend for rendering scenex visualizations.

    This function allows you to explicitly select which graphics library (backend)
    scenex should use for rendering. It is the goal of scenex to support the full range
    of model API for each backend.

    If not called, scenex will automatically select an arbitrary available backend. You
    can also set the backend via the SCENEX_CANVAS_BACKEND environment variable.

    Parameters
    ----------
    backend : Literal["pygfx", "vispy"] | None
        The graphics backend to use:
        - "pygfx": Modern WebGPU-based renderer with advanced features
        - "vispy": OpenGL-based renderer with broad compatibility
        - None: Reset to auto-detection

    Raises
    ------
    ValueError
        If the specified backend is not one of the known backends.

    Examples
    --------
    Use pygfx backend explicitly:

    >>> import scenex as snx
    >>> snx.use("pygfx")  # doctest: +SKIP
    >>> canvas = snx.show(snx.View())

    Use vispy backend:

    >>> snx.use("vispy")  # doctest: +SKIP
    >>> canvas = snx.show(snx.Scene())

    Reset to auto-detection:

    >>> snx.use(None)

    Notes
    -----
    The backend selection follows this priority order:
    1. Backend specified by this function
    2. SCENEX_CANVAS_BACKEND environment variable
    3. Auto-detection (pygfx preferred, then vispy)

    This function should be called before creating any visualizations.
    """
    global _USE
    if backend is None or _ensure_valid_backend(backend):
        _USE = backend