Source code for isaaclab.sim.views.xform_space_writer
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Context-managed transform writers for :class:`~isaaclab.sim.views.BaseFrameView`.
This module defines the recommended write API for FrameView poses and scales:
.. code-block:: python
with view.xform_world_space_writer() as writer:
writer.set_poses(positions=p, orientations=o)
writer.set_scales(scales=s)
# ... any number of writes ...
# On exit the writer derives the opposite-space matrices once,
# synchronizes once, and restores any saved Fabric tracking state.
Only one writer may be active per view at a time. While a writer scope is
active on a view, view-level getters (``view.get_world_poses``,
``view.get_local_poses``, ``view.get_world_scales``,
``view.get_local_scales``) raise :class:`RuntimeError` -- use the writer's own
:meth:`~FrameViewSpaceWriterBase.get_poses` / :meth:`~FrameViewSpaceWriterBase.get_scales`
inside the scope, or exit the scope first.
**Do not advance the simulation or render from inside a scope.** The scope
runs as synchronous Python code, so no ``sim.step()`` / ``world.render()`` /
``SimulationApp.update()`` is allowed inside the ``with`` block. Until the
scope exits, the backend's matrices may be mid-write (some prims updated,
others not; the opposite-space derive has not yet run) and rendering against
that state would read torn data. Keep scopes short and step the
simulation outside them.
"""
from __future__ import annotations
import abc
from typing import TYPE_CHECKING
import warp as wp
from isaaclab.utils.warp import ProxyArray
if TYPE_CHECKING:
from .base_frame_view import BaseFrameView
[docs]
class FrameViewSpaceWriterBase(abc.ABC):
"""Abstract context-managed writer for a single transform space.
Subclasses are returned by :meth:`BaseFrameView.xform_world_space_writer` /
:meth:`BaseFrameView.xform_local_space_writer`; they
are not constructed directly. The class is intentionally minimal -- the
pose/scale semantics depend on the writer's space (world or local), which
is conveyed by the concrete tag class :class:`FrameViewWorldSpaceWriter` or
:class:`FrameViewLocalSpaceWriter`.
The scope runs as synchronous Python code: no simulation step and no
render tick can run while it is open, and the caller must not advance
either from inside the ``with`` block. See the module docstring for the
full contract.
"""
[docs]
def __init__(self, view: BaseFrameView):
self._view = view
@abc.abstractmethod
def set_poses(
self,
positions: wp.array | None = None,
orientations: wp.array | None = None,
indices: wp.array | None = None,
) -> None:
"""Set positions and/or orientations in this writer's space.
Args:
positions: Positions ``(M, 3)``. ``None`` leaves positions unchanged.
orientations: Quaternions ``(M, 4)`` in ``(x, y, z, w)``.
``None`` leaves orientations unchanged.
indices: Subset of prims to update. ``None`` means all prims.
"""
...
@abc.abstractmethod
def set_scales(self, scales: wp.array, indices: wp.array | None = None) -> None:
"""Set scales in this writer's space.
Args:
scales: Scales ``(M, 3)`` as ``wp.array``.
indices: Subset of prims to update. ``None`` means all prims.
"""
...
@abc.abstractmethod
def get_poses(self, indices: wp.array | None = None) -> tuple[ProxyArray, ProxyArray]:
"""Return ``(positions, orientations)`` in this writer's space.
Reflects any in-scope writes that have already been queued on the
underlying device stream.
"""
...
@abc.abstractmethod
def get_scales(self, indices: wp.array | None = None) -> ProxyArray:
"""Return scales in this writer's space."""
...
def __enter__(self) -> FrameViewSpaceWriterBase:
if self._view._active_writer is not None:
raise RuntimeError(
f"{type(self._view).__name__} already has an active writer scope "
f"({type(self._view._active_writer).__name__}). Exit the existing scope before "
"opening a new one."
)
self._view._active_writer = self
self._enter_impl()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
try:
self._exit_impl(exc_type, exc_val, exc_tb)
finally:
self._view._active_writer = None
def _enter_impl(self) -> None:
"""Backend hook called after the single-active-writer lock is claimed."""
def _exit_impl(self, exc_type, exc_val, exc_tb) -> None:
"""Backend hook called before the single-active-writer lock is released."""
[docs]
class FrameViewWorldSpaceWriter(FrameViewSpaceWriterBase):
"""Writer whose :meth:`set_poses` / :meth:`set_scales` write world-space values.
On context exit the opposite-space (``local``) matrices are derived from
the just-written world matrices in a single Warp kernel launch.
"""
[docs]
class FrameViewLocalSpaceWriter(FrameViewSpaceWriterBase):
"""Writer whose :meth:`set_poses` / :meth:`set_scales` write local-space values.
On context exit the opposite-space (``world``) matrices are derived from
the just-written local matrices in a single Warp kernel launch.
"""