Slider Manipulator#

In this guide you will learn how to draw a 3D slider in the viewport that overlays on the top of the bounding box of the selected prim. This slider will control the scale of the prim with a custom manipulator, model, and gesture. When the slider is changed, the manipulator processes the custom gesture that changes the data in the model, which changes the data directly in the USD stage.

../_images/work_sliderPreview.png

Learning Objectives#

  • Create an extension

  • Import omni.ui and USD

  • Set up Model and Manipulator

  • Create Gestures

  • Create a working scale slider

Prerequisites#

To help understand the concepts used in this guide, it is recommended that you complete the following:

Warning

Check that Viewport Utility Extension is turned ON in the Extensions Manager:

Step 1: Create the extension#

In this section, you will create a new extension in Omniverse Code.

Step 1.1: Create new extension template#

In Omniverse Code navigate to the Extensions tab and create a new extension by clicking the ➕ icon in the upper left corner and select New Extension Template Project.

Plus Icon New Extension Template

A new extension template window and Visual Studio Code will open after you have selected the folder location, folder name, and extension ID.

Step 1.2: Naming your extension#

In the extension manager, you may have noticed that each extension has a title and description:

../_images/work_extensionManager_example.png

You can change this in the extension.toml file by navigating to VS Code and editing the file there. It is important that you give your extension a detailed title and summary for the end user to understand what your extension will accomplish or display. Here is how to change it for this guide:

# The title and description fields are primarily for displaying extension info in UI
title = "UI Scene Slider Manipulator"
description="Interactive example of the slider manipulator with omni.ui.scene"

Step 2: Model module#

In this step you will be creating the slider_model.py module where you will be tracking the current selected prim, listening to stage events, and getting the position directly from USD.

This module will be made up of many lines so be sure to review the “memo: Code Checkpoint” for updated code of the module at various steps.

Step 2.1: Import omni.ui and USD#

After creating slider_model.py in the same folder as extension.py, import scene from omni.ui and the necessary USD modules, as follows:

from omni.ui import scene as sc
from pxr import Tf
from pxr import Gf
from pxr import Usd
from pxr import UsdGeom
import omni.usd

Step 2.2: SliderModel and PositionItem Classes#

Next, let’s set up your SliderModel and PositionItem classes. SliderModel tracks the position and scale of the selected prim and PositionItem stores the position value.

from omni.ui import scene as sc
from pxr import Tf
from pxr import Gf
from pxr import Usd
from pxr import UsdGeom
import omni.usd

# NEW
class SliderModel(sc.AbstractManipulatorModel):
    """
    User part. The model tracks the position and scale of the selected
    object.
    """
    class PositionItem(sc.AbstractManipulatorItem):
        """
        The Model Item represents the position. It doesn't contain anything
        because because you take the position directly from USD when requesting.
        """

        def __init__(self):
            super().__init__()
            self.value = [0, 0, 0]

    def __init__(self) -> None:
        super().__init__()

        self.position = SliderModel.PositionItem()
# END NEW

Step 2.3: Current Selection and Tracking Selection#

In this section, you will be setting the variables for the current selection and tracking the selected prim, where you will also set parameters for the stage event later on.

...

class SliderModel(sc.AbstractManipulatorModel):
    """
    User part. The model tracks the position and scale of the selected
    object.
    """
    class PositionItem(sc.AbstractManipulatorItem):
        """
        The Model Item represents the position. It doesn't contain anything
        because because you take the position directly from USD when requesting.
        """

        def __init__(self):
            super().__init__()
            self.value = [0, 0, 0]

    def __init__(self) -> None:
        super().__init__()

        self.position = SliderModel.PositionItem()

        # NEW
        # Current selection
        self.current_path = ""
        self.stage_listener = None
        self.usd_context = omni.usd.get_context()
        self.stage: Usd.Stage = self.usd_context.get_stage()

        # Track selection
        self.selection = self.usd_context.get_selection()
        self.events = self.usd_context.get_stage_event_stream()
        self.stage_event_delegate = self.events.create_subscription_to_pop(
            self.on_stage_event, name="Slider Selection Update"
        )
        # END NEW
Click here for the updated code of SliderModel
from omni.ui import scene as sc
from pxr import Tf
from pxr import Gf
from pxr import Usd
from pxr import UsdGeom
import omni.usd


class SliderModel(sc.AbstractManipulatorModel):
    """
    User part. The model tracks the position and scale of the selected
    object.
    """
    class PositionItem(sc.AbstractManipulatorItem):
        """
        The Model Item represents the position. It doesn't contain anything
        because because you take the position directly from USD when requesting.
        """

        def __init__(self):
            super().__init__()
            self.value = [0, 0, 0]

    def __init__(self) -> None:
        super().__init__()

        self.position = SliderModel.PositionItem()

        # Current selection
        self.current_path = ""
        self.stage_listener = None
        self.usd_context = omni.usd.get_context()
        self.stage: Usd.Stage = self.usd_context.get_stage()

        # Track selection
        self.selection = self.usd_context.get_selection()
        self.events = self.usd_context.get_stage_event_stream()
        self.stage_event_delegate = self.events.create_subscription_to_pop(
            self.on_stage_event, name="Slider Selection Update"
        )

Step 2.4: Define on_stage_event()#

With your selection variables set, you now define the on_stage_event() call back to get the selected prim and its position on selection changes. You will start the new function for these below module previous code:

...


    def on_stage_event(self, event):
        """Called by stage_event_stream"""
        if event.type == int(omni.usd.StageEventType.SELECTION_CHANGED):
            prim_paths = self.selection.get_selected_prim_paths()
            if not prim_paths:
                self._item_changed(self.position)
                # Revoke the Tf.Notice listener, you don't need to update anything
                if self.stage_listener:
                    self.stage_listener.Revoke()
                    self.stage_listener = None
                return
            prim = self.stage.GetPrimAtPath(prim_paths[0])
            if not prim.IsA(UsdGeom.Imageable):
                return

            self.current_path = prim_paths[0]

            # Add a Tf.Notice listener to update the position
            if not self.stage_listener:
                self.stage_listener = Tf.Notice.Register(Usd.Notice.ObjectsChanged, self._notice_changed, self.stage)

            # Position is changed
            self._item_changed(self.position)
Click here for the updated code of SliderModel
 from omni.ui import scene as sc
 from pxr import Tf
 from pxr import Gf
 from pxr import Usd
 from pxr import UsdGeom
 import omni.usd


class SliderModel(sc.AbstractManipulatorModel):
    """
    User part. The model tracks the position and scale of the selected
    object.
    """
    class PositionItem(sc.AbstractManipulatorItem):
        """
        The Model Item represents the position. It doesn't contain anything
        because because you take the position directly from USD when requesting.
        """

        def __init__(self):
            super().__init__()
            self.value = [0, 0, 0]

    def __init__(self) -> None:
        super().__init__()

        self.position = SliderModel.PositionItem()

        # Current selection
        self.current_path = ""
        self.stage_listener = None
        self.usd_context = omni.usd.get_context()
        self.stage: Usd.Stage = self.usd_context.get_stage()

        # Track selection
        self.selection = self.usd_context.get_selection()
        self.events = self.usd_context.get_stage_event_stream()
        self.stage_event_delegate = self.events.create_subscription_to_pop(
            self.on_stage_event, name="Slider Selection Update"
        )

    def on_stage_event(self, event):
      """Called by stage_event_stream"""
        if event.type == int(omni.usd.StageEventType.SELECTION_CHANGED):
            prim_paths = self.selection.get_selected_prim_paths()
            if not prim_paths:
                self._item_changed(self.position)
                # Revoke the Tf.Notice listener, you don't need to update anything
                if self.stage_listener:
                    self.stage_listener.Revoke()
                    self.stage_listener = None
                return
            prim = self.stage.GetPrimAtPath(prim_paths[0])
            if not prim.IsA(UsdGeom.Imageable):
                return

            self.current_path = prim_paths[0]

            # Add a Tf.Notice listener to update the position
            if not self.stage_listener:
                self.stage_listener = Tf.Notice.Register(Usd.Notice.ObjectsChanged, self._notice_changed, self.stage)

            # Position is changed
            self._item_changed(self.position)

Step 2.5: Tf.Notice callback#

In the previous step, you registered a callback to be called when objects in the stage change. Click here for more information on Tf.Notice.

Now, you will define the callback function. You want to update the stored position of the selected prim. You can add that as follows:

...

    def _notice_changed(self, notice, stage):
        """Called by Tf.Notice"""
        for p in notice.GetChangedInfoOnlyPaths():
            if self.current_path in str(p.GetPrimPath()):
                self._item_changed(self.position)

Step 2.6: Set the Position Identifier and return Position#

Let’s define the identifier for position like so:

...

    def get_item(self, identifier):
        if identifier == "position":
            return self.position

And now, you will set item to return the position and get the value from the item:

...

    def get_as_floats(self, item):
        if item == self.position:
            # Requesting position
            return self.get_position()
        if item:
            # Get the value directly from the item
            return item.value
        return []
Click here for the updated code of SliderModel
from omni.ui import scene as sc
from pxr import Tf
from pxr import Gf
from pxr import Usd
from pxr import UsdGeom
import omni.usd


class SliderModel(sc.AbstractManipulatorModel):
    """
    User part. The model tracks the position and scale of the selected
    object.
    """
    class PositionItem(sc.AbstractManipulatorItem):
        """
        The Model Item represents the position. It doesn't contain anything
        because because you take the position directly from USD when requesting.
        """

        def __init__(self):
            super().__init__()
            self.value = [0, 0, 0]

    def __init__(self) -> None:
        super().__init__()

        self.position = SliderModel.PositionItem()

        # Current selection
        self.current_path = ""
        self.stage_listener = None
        self.usd_context = omni.usd.get_context()
        self.stage: Usd.Stage = self.usd_context.get_stage()

        # Track selection
        self.selection = self.usd_context.get_selection()
        self.events = self.usd_context.get_stage_event_stream()
        self.stage_event_delegate = self.events.create_subscription_to_pop(
            self.on_stage_event, name="Slider Selection Update"
        )

    def on_stage_event(self, event):
      """Called by stage_event_stream"""
        if event.type == int(omni.usd.StageEventType.SELECTION_CHANGED):
            prim_paths = self.selection.get_selected_prim_paths()
            if not prim_paths:
                self._item_changed(self.position)
                # Revoke the Tf.Notice listener, you don't need to update anything
                if self.stage_listener:
                    self.stage_listener.Revoke()
                    self.stage_listener = None
                return
            prim = self.stage.GetPrimAtPath(prim_paths[0])
            if not prim.IsA(UsdGeom.Imageable):
                return

            self.current_path = prim_paths[0]

            # Add a Tf.Notice listener to update the position
            if not self.stage_listener:
                self.stage_listener = Tf.Notice.Register(Usd.Notice.ObjectsChanged, self._notice_changed, self.stage)

            # Position is changed
            self._item_changed(self.position)

    def _notice_changed(self, notice, stage):
        """Called by Tf.Notice"""
        for p in notice.GetChangedInfoOnlyPaths():
            if self.current_path in str(p.GetPrimPath()):
                self._item_changed(self.position)

    def get_item(self, identifier):
        if identifier == "position":
            return self.position

    def get_as_floats(self, item):
        if item == self.position:
            # Requesting position
            return self.get_position()
        if item:
            # Get the value directly from the item
            return item.value
        return []

Step 2.7: Position from USD#

In this last section of slider_model.py, you will be defining get_position to compute position directly from USD, like so:

...
    def get_position(self):
        """Returns position of currently selected object"""
        if not self.current_path:
            return [0, 0, 0]

        # Get position directly from USD
        prim = self.stage.GetPrimAtPath(self.current_path)
        box_cache = UsdGeom.BBoxCache(Usd.TimeCode.Default(), includedPurposes=[UsdGeom.Tokens.default_])
        bound = box_cache.ComputeWorldBound(prim)
        range = bound.ComputeAlignedBox()
        bboxMin = range.GetMin()
        bboxMax = range.GetMax()

        x_Pos = (bboxMin[0] + bboxMax[0]) * 0.5
        y_Pos = (bboxMax[1] + 10)
        z_Pos = (bboxMin[2] + bboxMax[2]) * 0.5
        position = [x_Pos, y_Pos, z_Pos]
        return position
Click here for the updated code of SliderModel
from omni.ui import scene as sc
from pxr import Tf
from pxr import Gf
from pxr import Usd
from pxr import UsdGeom
import omni.usd

class SliderModel(sc.AbstractManipulatorModel):
    """
    User part. The model tracks the position and scale of the selected
    object.
    """
    class PositionItem(sc.AbstractManipulatorItem):
        """
        The Model Item represents the position. It doesn't contain anything
        because because you take the position directly from USD when requesting.
        """

        def __init__(self):
            super().__init__()
            self.value = [0, 0, 0]

    def __init__(self) -> None:
        super().__init__()

        self.position = SliderModel.PositionItem()

        # Current selection
        self.current_path = ""
        self.stage_listener = None
        self.usd_context = omni.usd.get_context()
        self.stage: Usd.Stage = self.usd_context.get_stage()

        # Track selection
        self.selection = self.usd_context.get_selection()
        self.events = self.usd_context.get_stage_event_stream()
        self.stage_event_delegate = self.events.create_subscription_to_pop(
            self.on_stage_event, name="Slider Selection Update"
        )

    def on_stage_event(self, event):
        """Called by stage_event_stream"""
        if event.type == int(omni.usd.StageEventType.SELECTION_CHANGED):
            prim_paths = self.selection.get_selected_prim_paths()
            if not prim_paths:
                self._item_changed(self.position)
                # Revoke the Tf.Notice listener, you don't need to update anything
                if self.stage_listener:
                    self.stage_listener.Revoke()
                    self.stage_listener = None
                return
            prim = self.stage.GetPrimAtPath(prim_paths[0])
            if not prim.IsA(UsdGeom.Imageable):
                return

            self.current_path = prim_paths[0]

            # Add a Tf.Notice listener to update the position
            if not self.stage_listener:
                self.stage_listener = Tf.Notice.Register(Usd.Notice.ObjectsChanged, self._notice_changed, self.stage)

            # Position is changed
            self._item_changed(self.position)


    def _notice_changed(self, notice, stage):
        """Called by Tf.Notice"""
        for p in notice.GetChangedInfoOnlyPaths():
            if self.current_path in str(p.GetPrimPath()):
                self._item_changed(self.position)

    def get_item(self, identifier):
        if identifier == "position":
            return self.position

    def get_as_floats(self, item):
        if item == self.position:
            # Requesting position
            return self.get_position()
        if item:
            # Get the value directly from the item
            return item.value
        return []

    def get_position(self):
        """Returns position of currently selected object"""
        if not self.current_path:
            return [0, 0, 0]

        # Get position directly from USD
        prim = self.stage.GetPrimAtPath(self.current_path)
        box_cache = UsdGeom.BBoxCache(Usd.TimeCode.Default(), includedPurposes=[UsdGeom.Tokens.default_])
        bound = box_cache.ComputeWorldBound(prim)
        range = bound.ComputeAlignedBox()
        bboxMin = range.GetMin()
        bboxMax = range.GetMax()

        x_Pos = (bboxMin[0] + bboxMax[0]) * 0.5
        y_Pos = (bboxMax[1] + 10)
        z_Pos = (bboxMin[2] + bboxMax[2]) * 0.5
        position = [x_Pos, y_Pos, z_Pos]
        return position

Step 3: Manipulator Module#

In this step, you will be creating slider_manipulator.py in th e same folder as slider_model.py. The Manipulator class will define on_build() as well as create the Label and regenerate the model.

Step 3.1: Import omni.ui#

After creating slider_manipulator.py, import omni.ui as follows:

from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui

Step 3.2: Create SliderManipulator class#

Now, you will begin the SliderManipulator class and define the __init__():

from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui


class SliderManipulator(sc.Manipulator):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

Step 3.3: Define on_build() and create the Label#

on_build() is called when the model is changed and it will rebuild the slider. You will also create the Label for the slider and position it more towards the top of the screen.

...

    def on_build(self):
        """Called when the model is changed and rebuilds the whole slider"""
        if not self.model:
            return

        # If you don't have a selection then just return
        if self.model.get_item("name") == "":
            return

        value = 0.0
        position = self.model.get_as_floats(self.model.get_item("position"))

        with sc.Transform(transform=sc.Matrix44.get_translation_matrix(*position)):

        # Label
            with sc.Transform(look_at=sc.Transform.LookAt.CAMERA):
                with sc.Transform(scale_to=sc.Space.SCREEN):
                # Move it 5 points more to the top in the screen space
                    with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 5, 0)):
                        sc.Label(f"{value:.1f}", alignment=ui.Alignment.CENTER_BOTTOM)

Step 3.4: Regenerate the Manipulator#

Finally, let’s define on_model_updated() to regenerate the manipulator:

...
    def on_model_updated(self, item):
        # Regenerate the manipulator
        self.invalidate()
Click here for the end code of slider_manipulator.py
from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui


class SliderManipulator(sc.Manipulator):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def on_build(self):
        """Called when the model is  changed and rebuilds the whole slider"""
        if not self.model:
            return

        # If you don't have a selection then just return
        if self.model.get_item("name") == "":
            return

        value = 0.0
        position = self.model.get_as_floats(self.model.get_item("position"))

        with sc.Transform(transform=sc.Matrix44.get_translation_matrix(*position)):

        # Label
            with sc.Transform(look_at=sc.Transform.LookAt.CAMERA):
                with sc.Transform(scale_to=sc.Space.SCREEN):
                # Move it 5 points more to the top in the screen space
                    with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 5, 0)):
                        sc.Label(f"{value:.1f}", alignment=ui.Alignment.CENTER_BOTTOM)

    def on_model_updated(self, item):
        # Regenerate the manipulator
        self.invalidate()

Step 4: Registry Module#

In this step, you will create slider_registry.py in the same location as the slider_manipulator.py. You will use slider_registry.py to have the number display on the screen when the prim is selected.

Step 4.1: Import from Model and Manipulator#

After creating slider_registry.py, import from the SliderModel and SliderManipulator, as well as import typing for type hinting, like so:

from .slider_model import SliderModel
from .slider_manipulator import SliderManipulator
from typing import Any
from typing import Dict
from typing import Optional

Step 4.2: Disable Selection in Viewport Legacy#

Your first class will address disabling the selection in viewport legacy but you may encounter a bug that will not set your focused window to True. As a result, you will operate all Viewport instances for a given usd_context instead:

...

class ViewportLegacyDisableSelection:
    """Disables selection in the Viewport Legacy"""

    def __init__(self):
        self._focused_windows = None
        focused_windows = []
        try:
            # For some reason is_focused may return False, when a Window is definitely in fact the focused window!
            # And there's no good solution to this when multiple Viewport-1 instances are open; so you just have to
            # operate on all Viewports for a given usd_context.
            import omni.kit.viewport_legacy as vp

            vpi = vp.acquire_viewport_interface()
            for instance in vpi.get_instance_list():
                window = vpi.get_viewport_window(instance)
                if not window:
                    continue
                focused_windows.append(window)
            if focused_windows:
                self._focused_windows = focused_windows
                for window in self._focused_windows:
                    # Disable the selection_rect, but enable_picking for snapping
                    window.disable_selection_rect(True)
        except Exception:
            pass

Step 4.3: SliderChangedGesture Class#

Under your previously defined ViewportLegacyDisableSelection class, you will define SliderChangedGesture class. In this class you will start with __init__() and then define on_began(), which will disable the selection rect when the user drags the slider:

class SliderChangedGesture(SliderManipulator.SliderChangedGesture):
    """User part. Called when slider is changed."""
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def on_began(self):
        # When the user drags the slider, you don't want to see the selection rect
        self.__disable_selection = ViewportLegacyDisableSelection()

Next in this class, you will define on_changed(), which will be called when the user moves the slider. This will update the mesh as the scale of the model is changed. You will also define on_ended() to re-enable the selection rect when the slider is not being dragged.

def on_changed(self):
    """Called when the user moved the slider"""
    if not hasattr(self.gesture_payload, "slider_value"):
        return
    # The current slider value is in the payload.
    slider_value = self.gesture_payload.slider_value
    # Change the model. Slider watches it and it will update the mesh.
    self.sender.model.set_floats(self.sender.model.get_item("value"), [slider_value])

def on_ended(self):
    # This re-enables the selection in the Viewport Legacy
    self.__disable_selection = None
Click here for the updated code of slider_registry.py
from .slider_model import SliderModel
from .slider_manipulator import SliderManipulator
from typing import Any
from typing import Dict
from typing import Optional


class ViewportLegacyDisableSelection:
    """Disables selection in the Viewport Legacy"""

    def __init__(self):
        self._focused_windows = None
        focused_windows = []
        try:
            # For some reason is_focused may return False, when a Window is definitely in fact the focused window!
            # And there's no good solution to this when multiple Viewport-1 instances are open; so you just have to
            # operate on all Viewports for a given usd_context.
            import omni.kit.viewport_legacy as vp

            vpi = vp.acquire_viewport_interface()
            for instance in vpi.get_instance_list():
                window = vpi.get_viewport_window(instance)
                if not window:
                    continue
                focused_windows.append(window)
            if focused_windows:
                self._focused_windows = focused_windows
                for window in self._focused_windows:
                    # Disable the selection_rect, but enable_picking for snapping
                    window.disable_selection_rect(True)
        except Exception:
            pass

class SliderChangedGesture(SliderManipulator.SliderChangedGesture):
    """User part. Called when slider is changed."""
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def on_began(self):
        # When the user drags the slider, you don't want to see the selection rect
        self.__disable_selection = ViewportLegacyDisableSelection()

    def on_changed(self):
        """Called when the user moved the slider"""
        if not hasattr(self.gesture_payload, "slider_value"):
            return
        # The current slider value is in the payload.
        slider_value = self.gesture_payload.slider_value
        # Change the model. Slider watches it and it will update the mesh.
        self.sender.model.set_floats(self.sender.model.get_item("value"), [slider_value])

    def on_ended(self):
        # This re-enables the selection in the Viewport Legacy
        self.__disable_selection = None

Step 4.4: SliderRegistry Class#

Now create SliderRegistry class after your previous functions.

This class is created by omni.kit.viewport.registry or omni.kit.manipulator.viewport per viewport and will keep the manipulator and some other properties that are needed in the viewport. You will set the SliderRegistry class after the class you made in the previous step. Included in this class are the __init__() methods for your manipulator and some getters and setters:

...
class SliderRegistry:
    """
    Created by omni.kit.viewport.registry or omni.kit.manipulator.viewport per
    viewport. Keeps the manipulator and some properties that are needed to the
    viewport.
    """

    def __init__(self, description: Optional[Dict[str, Any]] = None):
        self.__slider_manipulator = SliderManipulator(model=SliderModel(), gesture=SliderChangedGesture())

    def destroy(self):
        if self.__slider_manipulator:
            self.__slider_manipulator.destroy()
            self.__slider_manipulator = None

    # PrimTransformManipulator & TransformManipulator don't have their own visibility
    @property
    def visible(self):
        return True

    @visible.setter
    def visible(self, value):
        pass

    @property
    def categories(self):
        return ("manipulator",)

    @property
    def name(self):
        return "Example Slider Manipulator"
Click here for the updated code of slider_registry.py
from .slider_model import SliderModel
from .slider_manipulator import SliderManipulator
from typing import Any
from typing import Dict
from typing import Optional


class ViewportLegacyDisableSelection:
    """Disables selection in the Viewport Legacy"""

    def __init__(self):
        self._focused_windows = None
        focused_windows = []
        try:
            # For some reason is_focused may return False, when a Window is definitely in fact the focused window!
            # And there's no good solution to this when multiple Viewport-1 instances are open; so you just have to
            # operate on all Viewports for a given usd_context.
            import omni.kit.viewport_legacy as vp

            vpi = vp.acquire_viewport_interface()
            for instance in vpi.get_instance_list():
                window = vpi.get_viewport_window(instance)
                if not window:
                    continue
                focused_windows.append(window)
            if focused_windows:
                self._focused_windows = focused_windows
                for window in self._focused_windows:
                    # Disable the selection_rect, but enable_picking for snapping
                    window.disable_selection_rect(True)
        except Exception:
            pass

class SliderChangedGesture(SliderManipulator.SliderChangedGesture):
    """User part. Called when slider is changed."""
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def on_began(self):
        # When the user drags the slider, you don't want to see the selection rect
        self.__disable_selection = ViewportLegacyDisableSelection()

    def on_changed(self):
        """Called when the user moved the slider"""
        if not hasattr(self.gesture_payload, "slider_value"):
            return
        # The current slider value is in the payload.
        slider_value = self.gesture_payload.slider_value
        # Change the model. Slider watches it and it will update the mesh.
        self.sender.model.set_floats(self.sender.model.get_item("value"), [slider_value])

    def on_ended(self):
        # This re-enables the selection in the Viewport Legacy
        self.__disable_selection = None

class SliderRegistry:
    """
    Created by omni.kit.viewport.registry or omni.kit.manipulator.viewport per
    viewport. Keeps the manipulator and some properties that are needed to the
    viewport.
    """

    def __init__(self, description: Optional[Dict[str, Any]] = None):
        self.__slider_manipulator = SliderManipulator(model=SliderModel(), gesture=SliderChangedGesture())

    def destroy(self):
        if self.__slider_manipulator:
            self.__slider_manipulator.destroy()
            self.__slider_manipulator = None

    # PrimTransformManipulator & TransformManipulator don't have their own visibility
    @property
    def visible(self):
        return True

    @visible.setter
    def visible(self, value):
        pass

    @property
    def categories(self):
        return ("manipulator",)

    @property
    def name(self):
        return "Example Slider Manipulator"

Step 5: Update extension.py#

You still have the default code in extension.py so now you will update the code to reflect the modules you made. You can locate the extension.py in the exts folder hierarchy where you created slider_model.py and slider_manipulator.py.

Step 5.1: New extension.py Imports#

Let’s begin by updating the imports at the top of extension.py to include ManipulatorFactory, RegisterScene, and SliderRegistry so that you can use them later on:

import omni.ext
# NEW
from omni.kit.manipulator.viewport import ManipulatorFactory
from omni.kit.viewport.registry import RegisterScene
from .slider_registry import SliderRegistry
# END NEW

Step 5.2: References in on_startup#

In this step, you will remove the default code in on_startup and replace it with a reference to the slider_registry and slider_factory, like so:

...

class MyExtension(omni.ext.IExt):
    # ext_id is current extension id. It can be used with extension manager to query additional information, like where
    # this extension is located on filesystem.
    def on_startup(self, ext_id):
        # NEW
        self.slider_registry = RegisterScene(SliderRegistry, "omni.example.slider")
        self.slider_factory = ManipulatorFactory.create_manipulator(SliderRegistry)
        # END NEW

Step 5.3: Update on_shutdown#

Now, you need to properly shutdown the extension. Let’s remove the print statement and replace it with:

...

    def on_shutdown(self):
        # NEW
        ManipulatorFactory.destroy_manipulator(self.slider_factory)
        self.slider_factory = None
        self.slider_registry.destroy()
        self.slider_registry = None
        # END NEW
Click here for the end code of extension.py
import omni.ext
from omni.kit.manipulator.viewport import ManipulatorFactory
from omni.kit.viewport.registry import RegisterScene
from .slider_registry import SliderRegistry


class MyExtension(omni.ext.IExt):
    # ext_id is current extension id. It can be used with extension manager to query additional information, like where
    # this extension is located on filesystem.
    def on_startup(self, ext_id):
        self.slider_registry = RegisterScene(SliderRegistry, "omni.example.slider")
        self.slider_factory = ManipulatorFactory.create_manipulator(SliderRegistry)

    def on_shutdown(self):
        ManipulatorFactory.destroy_manipulator(self.slider_factory)
        self.slider_factory = None
        self.slider_registry.destroy()
        self.slider_registry = None

This is what you should see at this point in the viewport:

../_images/work_step5EndView.png

Step 6: Creating the Slider Widget#

Now that you have all of the variables and necessary properties referenced, let’s start to create the slider widget. You will begin by creating the geometry needed for the widget, like the line, and then you will add a circle to the line.

Step 6.1: Geometry Properties#

You are going to begin by adding new geometry to slider_manipulator.py. You will set the geometry properties in the __init__() like so:

from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui


class SliderManipulator(sc.Manipulator):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # NEW
        # Geometry properties
        self.width = 100
        self.thickness = 5
        self._radius = 5
        self._radius_hovered = 7
        # END NEW

Step 6.2: Create the line#

Next, you will create a line above the selected prim. Let’s add this to on_build():

...
    def on_build(self):
        """Called when the model is  changed and rebuilds the whole slider"""
        if not self.model:
            return

        # If you don't have a selection then just return
        if self.model.get_item("name") == "":
            return

        value = 0.0
        position = self.model.get_as_floats(self.model.get_item("position"))

        with sc.Transform(transform=sc.Matrix44.get_translation_matrix(*position)):

          # NEW
            # Left line
            line_from = -self.width * 0.5
            line_to = -self.width * 0.5 + self.width * 1 - self._radius
            if line_to > line_from:
                sc.Line([line_from, 0, 0], [line_to, 0, 0], color=cl.darkgray, thickness=self.thickness)
            # END NEW

        # Label
            with sc.Transform(look_at=sc.Transform.LookAt.CAMERA):
                with sc.Transform(scale_to=sc.Space.SCREEN):
                # Move it 5 points more to the top in the screen space
                    with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 5, 0)):
                        sc.Label(f"{value:.1f}", alignment=ui.Alignment.CENTER_BOTTOM)

This should be the result in your viewport:

../_images/work_step6Result.png

Step 6.3: Create the circle#

You are still working in slider_manipulator.py and now you will be adding the circle on the line for the slider. This will also be added to on_build() like so:

...
   def on_build(self):
        """Called when the model is changed and rebuilds the whole slider"""
        if not self.model:
            return

        # If you don't have a selection then just return
        if self.model.get_item("name") == "":
            return

        value = 0.0
        position = self.model.get_as_floats(self.model.get_item("position"))

        with sc.Transform(transform=sc.Matrix44.get_translation_matrix(*position)):
            # Left line
            line_from = -self.width * 0.5
            line_to = -self.width * 0.5 + self.width * 1 - self.radius
            if line_to > line_from:
                sc.Line([line_from, 0, 0], [line_to, 0, 0], color=cl.darkgray, thickness=self.thickness)

            # NEW
            # Circle
            circle_position = -self.width * 0.5 + self.width * 1
            with sc.Transform(transform=sc.Matrix44.get_translation_matrix(circle_position, 0, 0)):
                radius = self._radius
                sc.Arc(radius, axis=2, color=cl.gray)
            # END NEW
...

Now, your line in your viewport should look like this:

../_images/work_step6CircleResult.png

Click here for the full slider_manipulatory.py

from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui


class SliderManipulator(sc.Manipulator):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # Geometry properties
        self.width = 100
        self.thickness = 5
        self.radius = 5
        self.radius_hovered = 7

    def on_build(self):
        """Called when the model is  changed and rebuilds the whole slider"""
        if not self.model:
            return

        # If you don't have a selection then just return
        if self.model.get_item("name") == "":
            return

        value = 0.0
        position = self.model.get_as_floats(self.model.get_item("position"))

        with sc.Transform(transform=sc.Matrix44.get_translation_matrix(*position)):
            # Left line
            line_from = -self.width * 0.5
            line_to = -self.width * 0.5 + self.width * 1 - self._radius
            if line_to > line_from:
                sc.Line([line_from, 0, 0], [line_to, 0, 0], color=cl.darkgray, thickness=self.thickness)

            # Circle
            circle_position = -self.width * 0.5 + self.width * 1
            with sc.Transform(transform=sc.Matrix44.get_translation_matrix(circle_position, 0, 0)):
                radius = self._radius
                sc.Arc(radius, axis=2, color=cl.gray)

            # Label
            with sc.Transform(look_at=sc.Transform.LookAt.CAMERA):
                with sc.Transform(scale_to=sc.Space.SCREEN):
                    # Move it 5 points more to the top in the screen space
                    with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 5, 0)):
                        sc.Label(f"{value:.1f}", alignment=ui.Alignment.CENTER_BOTTOM)

    def on_model_updated(self, item):
        # Regenerate the manipulator
        self.invalidate()

Step 7: Set up the Model#

For this step, you will need to set up SliderModel to hold the information you need for the size of the selected prim. You will later use this information to connect it to the Manipulator.

Step 7.1: Import Omniverse Command Library#

First, let’s start by importing the Omniverse Command Library in slider_model.py

from omni.ui import scene as sc
from pxr import Tf
from pxr import Gf
from pxr import Usd
from pxr import UsdGeom
import omni.usd
# NEW IMPORT
import omni.kit.commands
# END NEW

Step 7.2: ValueItem Class#

Next, you will add a new Manipulator Item class, which you will name ValueItem, like so:

...
class SliderModel(sc.AbstractManipulatorModel):
    """
    User part. The model tracks the position and scale of the selected
    object.
    """
    class PositionItem(sc.AbstractManipulatorItem):
        """
        The Model Item represents the position. It doesn't contain anything
        because because you take the position directly from USD when requesting.
        """

        def __init__(self):
            super().__init__()
            self.value = [0, 0, 0]

    # NEW MANIPULATOR ITEM
    class ValueItem(sc.AbstractManipulatorItem):
        """The Model Item contains a single float value"""

        def __init__(self, value=0):
            super().__init__()
            self.value = [value]
    # END NEW
   ...

You will use this new class to create the variables for the min and max of the scale:

...
    class ValueItem(sc.AbstractManipulatorItem):
        """The Model Item contains a single float value"""

        def __init__(self, value=0):
            super().__init__()
            self.value = [value]

    def __init__(self) -> None:
        super().__init__()

        # NEW
        self.scale = SliderModel.ValueItem()
        self.min = SliderModel.ValueItem()
        self.max = SliderModel.ValueItem(1)
        # END NEW

        self.position = SliderModel.PositionItem()
   ...

Step 7.3: Set Scale to Stage#

With the new variables for the scale, populate them in on_stage_event() like so:

...
    def on_stage_event(self, event):
        """Called by stage_event_stream"""
        if event.type == int(omni.usd.StageEventType.SELECTION_CHANGED):
            prim_paths = self.selection.get_selected_prim_paths()
            if not prim_paths:
                self._item_changed(self.position)
                # Revoke the Tf.Notice listener, you don't need to update anything
                if self.stage_listener:
                    self.stage_listener.Revoke()
                    self.stage_listener = None
                return
            prim = self.stage.GetPrimAtPath(prim_paths[0])
            if not prim.IsA(UsdGeom.Imageable):
                return

            self.current_path = prim_paths[0]

            # NEW
            (old_scale, old_rotation_euler, old_rotation_order, old_translation) = omni.usd.get_local_transform_SRT(prim)

            scale = old_scale[0]
            _min = scale * 0.1
            _max = scale * 2.0
            self.set_floats(self.min, [_min])
            self.set_floats(self.max, [_max])
            self.set_floats(self.scale, [scale])
            # END NEW

            # Add a Tf.Notice listener to update the position
            if not self.stage_listener:
                self.stage_listener = Tf.Notice.Register(Usd.Notice.ObjectsChanged, self._notice_changed, self.stage)

            # Position is changed
            self._item_changed(self.position)
      ...
Click here for the updated code of slider_model.py
from omni.ui import scene as sc
from pxr import Tf
from pxr import Gf
from pxr import Usd
from pxr import UsdGeom
import omni.usd
import omni.kit.commands


class SliderModel(sc.AbstractManipulatorModel):
    """
    User part. The model tracks the position and scale of the selected
    object.
    """
    class PositionItem(sc.AbstractManipulatorItem):
        """
        The Model Item represents the position. It doesn't contain anything
        because because you take the position directly from USD when requesting.
        """

        def __init__(self):
            super().__init__()
            self.value = [0, 0, 0]

    class ValueItem(sc.AbstractManipulatorItem):
        """The Model Item contains a single float value"""

        def __init__(self, value=0):
            super().__init__()
            self.value = [value]

    def __init__(self) -> None:
        super().__init__()

        self.scale = SliderModel.ValueItem()
        self.min = SliderModel.ValueItem()
        self.max = SliderModel.ValueItem(1)

        self.position = SliderModel.PositionItem()

        # Current selection
        self.current_path = ""
        self.stage_listener = None
        self.usd_context = omni.usd.get_context()
        self.stage: Usd.Stage = self.usd_context.get_stage()

        # Track selection
        self.selection = self.usd_context.get_selection()
        self.events = self.usd_context.get_stage_event_stream()
        self.stage_event_delegate = self.events.create_subscription_to_pop(
            self.on_stage_event, name="Slider Selection Update"
        )

    def on_stage_event(self, event):
        """Called by stage_event_stream"""
        if event.type == int(omni.usd.StageEventType.SELECTION_CHANGED):
            prim_paths = self.selection.get_selected_prim_paths()
            if not prim_paths:
                self._item_changed(self.position)
                # Revoke the Tf.Notice listener, you don't need to update anything
                if self.stage_listener:
                    self.stage_listener.Revoke()
                    self.stage_listener = None
                return
            prim = self.stage.GetPrimAtPath(prim_paths[0])
            if not prim.IsA(UsdGeom.Imageable):
                return

            self.current_path = prim_paths[0]

            (old_scale, old_rotation_euler, old_rotation_order, old_translation) = omni.usd.get_local_transform_SRT(prim)

            scale = old_scale[0]
            _min = scale * 0.1
            _max = scale * 2.0
            self.set_floats(self.min, [_min])
            self.set_floats(self.max, [_max])
            self.set_floats(self.scale, [scale])

            # Add a Tf.Notice listener to update the position
            if not self.stage_listener:
                self.stage_listener = Tf.Notice.Register(Usd.Notice.ObjectsChanged, self._notice_changed, self.stage)

            # Position is changed
            self._item_changed(self.position)

    def _notice_changed(self, notice, stage):
        """Called by Tf.Notice"""
        for p in notice.GetChangedInfoOnlyPaths():
            if self.current_path in str(p.GetPrimPath()):
                self._item_changed(self.position)

    def get_item(self, identifier):
        if identifier == "position":
            return self.position

    def get_as_floats(self, item):
        if item == self.position:
            # Requesting position
            return self.get_position()
        if item:
            # Get the value directly from the item
            return item.value
        return []

    def get_position(self):
        """Returns position of currently selected object"""
        if not self.current_path:
            return [0, 0, 0]

        # Get position directly from USD
        prim = self.stage.GetPrimAtPath(self.current_path)
        box_cache = UsdGeom.BBoxCache(Usd.TimeCode.Default(), includedPurposes=[UsdGeom.Tokens.default_])
        bound = box_cache.ComputeWorldBound(prim)
        range = bound.ComputeAlignedBox()
        bboxMin = range.GetMin()
        bboxMax = range.GetMax()

        x_Pos = (bboxMin[0] + bboxMax[0]) * 0.5
        y_Pos = (bboxMax[1] + 10)
        z_Pos = (bboxMin[2] + bboxMax[2]) * 0.5
        position = [x_Pos, y_Pos, z_Pos]
        return position

Step 7.4: Define Identifiers#

Just as you defined the identifier for position, you must do the same for value, min, and max. You will add these to get_item:

...
    def get_item(self, identifier):
        if identifier == "position":
            return self.position
        # NEW
        if identifier == "value":
            return self.scale
        if identifier == "min":
            return self.min
        if identifier == "max":
            return self.max
        # END NEW
   ...

Step 7.5: Set Floats#

Previously, you called set_floats(), now define it after get_item(). In this function, you will set the scale when setting the value, set directly to the item, and update the manipulator:

def set_floats(self, item, value):
    if not self.current_path:
        return

    if not value or not item or item.value == value:
        return

    if item == self.scale:
        # Set the scale when setting the value.
        value[0] = min(max(value[0], self.min.value[0]), self.max.value[0])
        (old_scale, old_rotation_euler, old_rotation_order, old_translation) = omni.usd.get_local_transform_SRT(
            self.stage.GetPrimAtPath(self.current_path)
        )
        omni.kit.commands.execute(
            "TransformPrimSRTCommand",
            path=self.current_path,
            new_translation=old_translation,
            new_rotation_euler=old_rotation_euler,
            new_scale=Gf.Vec3d(value[0], value[0], value[0]),
        )

    # Set directly to the item
    item.value = value
    # This makes the manipulator updated
    self._item_changed(item)
Click here for the end code of slider_model.py
from omni.ui import scene as sc
from pxr import Tf
from pxr import Gf
from pxr import Usd
from pxr import UsdGeom
import omni.usd
import omni.kit.commands


class SliderModel(sc.AbstractManipulatorModel):
    """
    User part. The model tracks the position and scale of the selected
    object.
    """
    class PositionItem(sc.AbstractManipulatorItem):
        """
        The Model Item represents the position. It doesn't contain anything
        because because you take the position directly from USD when requesting.
        """

        def __init__(self):
            super().__init__()
            self.value = [0, 0, 0]


    class ValueItem(sc.AbstractManipulatorItem):
        """The Model Item contains a single float value"""

        def __init__(self, value=0):
            super().__init__()
            self.value = [value]


    def __init__(self) -> None:
        super().__init__()


        self.scale = SliderModel.ValueItem()
        self.min = SliderModel.ValueItem()
        self.max = SliderModel.ValueItem(1)

        self.position = SliderModel.PositionItem()

        # Current selection
        self.current_path = ""
        self.stage_listener = None
        self.usd_context = omni.usd.get_context()
        self.stage: Usd.Stage = self.usd_context.get_stage()

        # Track selection
        self.selection = self.usd_context.get_selection()
        self.events = self.usd_context.get_stage_event_stream()
        self.stage_event_delegate = self.events.create_subscription_to_pop(
            self.on_stage_event, name="Slider Selection Update"
        )

    def on_stage_event(self, event):
        """Called by stage_event_stream"""
        if event.type == int(omni.usd.StageEventType.SELECTION_CHANGED):
            prim_paths = self.selection.get_selected_prim_paths()
            if not prim_paths:
                self._item_changed(self.position)
                # Revoke the Tf.Notice listener, you don't need to update anything
                if self.stage_listener:
                    self.stage_listener.Revoke()
                    self.stage_listener = None
                return
            prim = self.stage.GetPrimAtPath(prim_paths[0])
            if not prim.IsA(UsdGeom.Imageable):
                return

            self.current_path = prim_paths[0]

            (old_scale, old_rotation_euler, old_rotation_order, old_translation) = omni.usd.get_local_transform_SRT(prim)

            scale = old_scale[0]
            _min = scale * 0.1
            _max = scale * 2.0
            self.set_floats(self.min, [_min])
            self.set_floats(self.max, [_max])
            self.set_floats(self.scale, [scale])

            # Add a Tf.Notice listener to update the position
            if not self.stage_listener:
                self.stage_listener = Tf.Notice.Register(Usd.Notice.ObjectsChanged, self._notice_changed, self.stage)

            # Position is changed
            self._item_changed(self.position)

    def _notice_changed(self, notice, stage):
        """Called by Tf.Notice"""
        for p in notice.GetChangedInfoOnlyPaths():
            if self.current_path in str(p.GetPrimPath()):
                self._item_changed(self.position)

    def get_item(self, identifier):
        if identifier == "position":
            return self.position
        if identifier == "value":
            return self.scale
        if identifier == "min":
            return self.min
        if identifier == "max":
            return self.max

    def set_floats(self, item, value):
        if not self.current_path:
            return

        if not value or not item or item.value == value:
            return

        if item == self.scale:
            # Set the scale when setting the value.
            value[0] = min(max(value[0], self.min.value[0]), self.max.value[0])
            (old_scale, old_rotation_euler, old_rotation_order, old_translation) = omni.usd.get_local_transform_SRT(
                self.stage.GetPrimAtPath(self.current_path)
            )
            omni.kit.commands.execute(
                "TransformPrimSRTCommand",
                path=self.current_path,
                new_translation=old_translation,
                new_rotation_euler=old_rotation_euler,
                new_scale=Gf.Vec3d(value[0], value[0], value[0]),
            )

        # Set directly to the item
        item.value = value
        # This makes the manipulator updated
        self._item_changed(item)

    def get_as_floats(self, item):
        if item == self.position:
            # Requesting position
            return self.get_position()
        if item:
            # Get the value directly from the item
            return item.value
        return []

    def get_position(self):
        """Returns position of currently selected object"""
        if not self.current_path:
            return [0, 0, 0]

        # Get position directly from USD
        prim = self.stage.GetPrimAtPath(self.current_path)
        box_cache = UsdGeom.BBoxCache(Usd.TimeCode.Default(), includedPurposes=[UsdGeom.Tokens.default_])
        bound = box_cache.ComputeWorldBound(prim)
        range = bound.ComputeAlignedBox()
        bboxMin = range.GetMin()
        bboxMax = range.GetMax()

        x_Pos = (bboxMin[0] + bboxMax[0]) * 0.5
        y_Pos = (bboxMax[1] + 10)
        z_Pos = (bboxMin[2] + bboxMax[2]) * 0.5
        position = [x_Pos, y_Pos, z_Pos]
        return position

Step 8: Add Gestures#

For your final step, you will be updating slider_manipulator.py to add the gestures needed to connect what you programmed in the Model. This will include checking that the gesture is not prevented during drag, calling the gesture, restructure the geometry properties, and update the Line and Circle.

Step 8.1: SliderDragGesturePayload Class#

Begin by creating a new class that the user will access to get the current value of the slider, like so:

from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui


class SliderManipulator(sc.Manipulator):

    # NEW
    class SliderDragGesturePayload(sc.AbstractGesture.GesturePayload):
        """
        Public payload. The user will access it to get the current value of
        the slider.
        """

        def __init__(self, base):
            super().__init__(base.item_closest_point, base.ray_closest_point, base.ray_distance)
            self.slider_value = 0
      ## END NEW

    ...

Step 8.2 SliderChangedGesture Class#

Next, you will create another new class that the user will reimplement to process the manipulator’s callbacks, in addition to a new __init__():

...
class SliderManipulator(sc.Manipulator):


    class SliderDragGesturePayload(sc.AbstractGesture.GesturePayload):
        """
        Public payload. The user will access it to get the current value of
        the slider.
        """

        def __init__(self, base):
            super().__init__(base.item_closest_point, base.ray_closest_point, base.ray_distance)
            self.slider_value = 0

# NEW
    class SliderChangedGesture(sc.ManipulatorGesture):
        """
        Public Gesture. The user will reimplement it to process the
        manipulator's callbacks.
        """

        def __init__(self, **kwargs):
            super().__init__(**kwargs)
# END NEW

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.width = 100
        self.thickness = 5
        self._radius = 5
        self._radius_hovered = 7
...

Nested inside of the SliderChangedGesture class, define process() directly after the __init__() definition of this class:

...
    class SliderChangedGesture(sc.ManipulatorGesture):
        """
        Public Gesture. The user will reimplement it to process the
        manipulator's callbacks.
        """

        def __init__(self, **kwargs):
            super().__init__(**kwargs)

            # NEW
        def process(self):
            # Redirection to methods
            if self.state == sc.GestureState.BEGAN:
                self.on_began()
            elif self.state == sc.GestureState.CHANGED:
                self.on_changed()
            elif self.state == sc.GestureState.ENDED:
                self.on_ended()
                # END NEW

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.width = 100
        self.thickness = 5
        self._radius = 5
        self._radius_hovered = 7
Click here for the updated code of slider_manipulator.py
from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui


class SliderManipulator(sc.Manipulator):

    class SliderDragGesturePayload(sc.AbstractGesture.GesturePayload):
        """
        Public payload. The user will access it to get the current value of
        the slider.
        """

        def __init__(self, base):
            super().__init__(base.item_closest_point, base.ray_closest_point, base.ray_distance)
            self.slider_value = 0

    class SliderChangedGesture(sc.ManipulatorGesture):
        """
        Public Gesture. The user will reimplement it to process the
        manipulator's callbacks.
        """

        def __init__(self, **kwargs):
            super().__init__(**kwargs)

        def process(self):
            # Redirection to methods
            if self.state == sc.GestureState.BEGAN:
                self.on_began()
            elif self.state == sc.GestureState.CHANGED:
                self.on_changed()
            elif self.state == sc.GestureState.ENDED:
                self.on_ended()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.width = 100
        self.thickness = 5
        self._radius = 5
        self._radius_hovered = 7

    def on_build(self):
        """Called when the model is  changed and rebuilds the whole slider"""
        if not self.model:
            return

        # If you don't have a selection then just return
        if self.model.get_item("name") == "":
            return

        value = 0.0
        position = self.model.get_as_floats(self.model.get_item("position"))

        with sc.Transform(transform=sc.Matrix44.get_translation_matrix(*position)):

            # Left line
            line_from = -self.width * 0.5
            line_to = -self.width * 0.5 + self.width * 1 - self._radius
            if line_to > line_from:
                sc.Line([line_from, 0, 0], [line_to, 0, 0], color=cl.darkgray, thickness=self.thickness)

            # Circle
            circle_position = -self.width * 0.5 + self.width * 1
            with sc.Transform(transform=sc.Matrix44.get_translation_matrix(circle_position, 0, 0)):
                radius = self._radius
                sc.Arc(radius, axis=2, color=cl.gray)

        # Label
        with sc.Transform(look_at=sc.Transform.LookAt.CAMERA):
            with sc.Transform(scale_to=sc.Space.SCREEN):
                # Move it 5 points more to the top in the screen space
                with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 5, 0)):
                    sc.Label(f"{value:.1f}", alignment=ui.Alignment.CENTER_BOTTOM)

    def on_model_updated(self, item):
        # Regenerate the manipulator
        self.invalidate()

Now, you need to define a few of the Public API functions after the process function:

    def process(self):
        # Redirection to methods
        if self.state == sc.GestureState.BEGAN:
            self.on_began()
        elif self.state == sc.GestureState.CHANGED:
            self.on_changed()
        elif self.state == sc.GestureState.ENDED:
            self.on_ended()

# NEW
    # Public API:
    def on_began(self):
        pass

    def on_changed(self):
        pass

    def on_ended(self):
        pass
# END NEW

Step 8.3 _ArcGesturePrioritize Class#

You will be adding an _ArcGesture class in the next step that needs the manager _ArcGesturePrioritize to make it the priority gesture. You will add the manager first to make sure the drag of the slider is not prevented during drag. You will slot this new class after your Public API functions:

        # Public API:
        def on_began(self):
            pass

        def on_changed(self):
            pass

        def on_ended(self):
            pass

# NEW
    class _ArcGesturePrioritize(sc.GestureManager):
        """
        Manager makes _ArcGesture the priority gesture
        """

        def can_be_prevented(self, gesture):
            # Never prevent in the middle of drag
            return gesture.state != sc.GestureState.CHANGED

        def should_prevent(self, gesture, preventer):
            if isinstance(preventer, SliderManipulator._ArcGesture):
                if preventer.state == sc.GestureState.BEGAN or preventer.state == sc.GestureState.CHANGED:
                    return True
# END NEW

Step 8.4: _ArcGesture Class#

Now, create the class _ArcGesture where you will set the new slider value and redirect to SliderChangedGesture class you made previously. This new class will be after the ArcGesturePrioritize manager class.

    class _ArcGesturePrioritize(sc.GestureManager):
        """
        Manager makes _ArcGesture the priority gesture
        """

        def can_be_prevented(self, gesture):
            # Never prevent in the middle of drag
            return gesture.state != sc.GestureState.CHANGED

        def should_prevent(self, gesture, preventer):
            if isinstance(preventer, SliderManipulator._ArcGesture):
                if preventer.state == sc.GestureState.BEGAN or preventer.state == sc.GestureState.CHANGED:
                    return True

# NEW
    class _ArcGesture(sc.DragGesture):
        """
        Internal gesture that sets the new slider value and redirects to
        public SliderChangedGesture.
        """

        def __init__(self, manipulator):
            super().__init__(manager=SliderManipulator._ArcGesturePrioritize())
            self._manipulator = manipulator

        def __repr__(self):
            return f"<_ArcGesture at {hex(id(self))}>"

        def process(self):
            if self.state in [sc.GestureState.BEGAN, sc.GestureState.CHANGED, sc.GestureState.ENDED]:
                # Form new gesture_payload object
                new_gesture_payload = SliderManipulator.SliderDragGesturePayload(self.gesture_payload)
                # Save the new slider position in the gesture_payload object
                object_ray_point = self._manipulator.transform_space(
                    sc.Space.WORLD, sc.Space.OBJECT, self.gesture_payload.ray_closest_point
                )
                center = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("position"))
                slider_value = (object_ray_point[0] - center[0]) / self._manipulator.width + 0.5
                _min = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("min"))[0]
                _max = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("max"))[0]
                new_gesture_payload.slider_value = _min + slider_value * (_max - _min)
                # Call the public gesture
                self._manipulator._process_gesture(
                    SliderManipulator.SliderChangedGesture, self.state, new_gesture_payload
                )
            # Base process of the gesture
            super().process()
# END NEW
Click here for the updated code of slider_manipulator.py
from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui


class SliderManipulator(sc.Manipulator):

    class SliderDragGesturePayload(sc.AbstractGesture.GesturePayload):
        """
        Public payload. The user will access it to get the current value of
        the slider.
        """

        def __init__(self, base):
            super().__init__(base.item_closest_point, base.ray_closest_point, base.ray_distance)
            self.slider_value = 0

    class SliderChangedGesture(sc.ManipulatorGesture):
        """
        Public Gesture. The user will reimplement it to process the
        manipulator's callbacks.
        """

        def __init__(self, **kwargs):
            super().__init__(**kwargs)

        def process(self):
            # Redirection to methods
            if self.state == sc.GestureState.BEGAN:
                self.on_began()
            elif self.state == sc.GestureState.CHANGED:
                self.on_changed()
            elif self.state == sc.GestureState.ENDED:
                self.on_ended()

        # Public API:
        def on_began(self):
            pass

        def on_changed(self):
            pass

        def on_ended(self):
            pass

    class _ArcGesturePrioritize(sc.GestureManager):
        """
        Manager makes _ArcGesture the priority gesture
        """

        def can_be_prevented(self, gesture):
            # Never prevent in the middle of drag
            return gesture.state != sc.GestureState.CHANGED

        def should_prevent(self, gesture, preventer):
            if isinstance(preventer, SliderManipulator._ArcGesture):
                if preventer.state == sc.GestureState.BEGAN or preventer.state == sc.GestureState.CHANGED:
                    return True

    class _ArcGesture(sc.DragGesture):
        """
        Internal gesture that sets the new slider value and redirects to
        public SliderChangedGesture.
        """

        def __init__(self, manipulator):
            super().__init__(manager=SliderManipulator._ArcGesturePrioritize())
            self._manipulator = manipulator

        def __repr__(self):
            return f"<_ArcGesture at {hex(id(self))}>"

        def process(self):
            if self.state in [sc.GestureState.BEGAN, sc.GestureState.CHANGED, sc.GestureState.ENDED]:
                # Form new gesture_payload object
                new_gesture_payload = SliderManipulator.SliderDragGesturePayload(self.gesture_payload)
                # Save the new slider position in the gesture_payload object
                object_ray_point = self._manipulator.transform_space(
                    sc.Space.WORLD, sc.Space.OBJECT, self.gesture_payload.ray_closest_point
                )
                center = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("position"))
                slider_value = (object_ray_point[0] - center[0]) / self._manipulator.width + 0.5
                _min = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("min"))[0]
                _max = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("max"))[0]
                new_gesture_payload.slider_value = _min + slider_value * (_max - _min)
                # Call the public gesture
                self._manipulator._process_gesture(
                    SliderManipulator.SliderChangedGesture, self.state, new_gesture_payload
                )
            # Base process of the gesture
            super().process()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.width = 100
        self.thickness = 5
        self._radius = 5
        self._radius_hovered = 7

    def on_build(self):
        """Called when the model is  changed and rebuilds the whole slider"""
        if not self.model:
            return

        # If you don't have a selection then just return
        if self.model.get_item("name") == "":
            return

        value = 0.0
        position = self.model.get_as_floats(self.model.get_item("position"))

        with sc.Transform(transform=sc.Matrix44.get_translation_matrix(*position)):

            # Left line
            line_from = -self.width * 0.5
            line_to = -self.width * 0.5 + self.width * 1 - self._radius
            if line_to > line_from:
                sc.Line([line_from, 0, 0], [line_to, 0, 0], color=cl.darkgray, thickness=self.thickness)

            # Circle
            circle_position = -self.width * 0.5 + self.width * 1
            with sc.Transform(transform=sc.Matrix44.get_translation_matrix(circle_position, 0, 0)):
                radius = self._radius
                sc.Arc(radius, axis=2, color=cl.gray)

        # Label
        with sc.Transform(look_at=sc.Transform.LookAt.CAMERA):
            with sc.Transform(scale_to=sc.Space.SCREEN):
                # Move it 5 points more to the top in the screen space
                with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 5, 0)):
                    sc.Label(f"{value:.1f}", alignment=ui.Alignment.CENTER_BOTTOM)

    def on_model_updated(self, item):
        # Regenerate the manipulator
        self.invalidate()

Step 8.5: Restructure Geometry Parameters#

For this step, you will be adding to __init__() that nests your Geometry properties, such as width,thickness,radius, and radius_hovered.

Tip

If you are having trouble locating the geometry properties, be reminded that this __init__() is after the new classes you added in the previous steps. You should find it under “_ArcGesture”

Start by defining set_radius() for the circle so that you can change it on hover later, and also set the parameters for arc_gesture to make sure it’s active when the object is recreated:

def __init__(self, **kwargs):
    super().__init__(**kwargs)
    # Geometry properties
    self._width = 100
    self._thickness = 5
    self._radius = 5
    self._radius_hovered = 7

  # NEW
    def set_radius(circle, radius):
        circle.radius = radius

    # You don't recreate the gesture to make sure it's active when the
    # underlying object is recreated
    self._arc_gesture = self._ArcGesture(self)
  # END NEW

Step 8.6: Add Hover Gestures#

Now that you have set the geometry properties for when you hover over them, create the HoverGesture instance. You will set this within an if statement under the parameters for self._arc_gesture:

    # You don't recreate the gesture to make sure it's active when the
    # underlying object is recreated
    self._arc_gesture = self._ArcGesture(self)

 # NEW
    if hasattr(sc, "HoverGesture"):
        self._hover_gesture = sc.HoverGesture(
            on_began_fn=lambda sender: set_radius(sender, self._radius_hovered),
            on_ended_fn=lambda sender: set_radius(sender, self._radius),
        )
    else:
        self._hover_gesture = None
# END NEW

Step 8.7: UI Getters and Setters#

Before moving on, you need to add a few Python decorators for the UI, such as @property,@width.setter and @height.setter. These can be added after the HoverGesture statement from the step above:

def destroy(self):
    pass

@property
def width(self):
    return self._width

@width.setter
def width(self, value):
    self._width = value
    # Regenerate the mesh
    self.invalidate()

@property
def thickness(self):
    return self._thickness

@thickness.setter
def thickness(self, value):
    self._thickness = value
    # Regenerate the mesh
    self.invalidate()
Click here for the updated code of slider_manipulator.py
from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui


class SliderManipulator(sc.Manipulator):

    class SliderDragGesturePayload(sc.AbstractGesture.GesturePayload):
        """
        Public payload. The user will access it to get the current value of
        the slider.
        """

        def __init__(self, base):
            super().__init__(base.item_closest_point, base.ray_closest_point, base.ray_distance)
            self.slider_value = 0

    class SliderChangedGesture(sc.ManipulatorGesture):
        """
        Public Gesture. The user will reimplement it to process the
        manipulator's callbacks.
        """

        def __init__(self, **kwargs):
            super().__init__(**kwargs)

        def process(self):
            # Redirection to methods
            if self.state == sc.GestureState.BEGAN:
                self.on_began()
            elif self.state == sc.GestureState.CHANGED:
                self.on_changed()
            elif self.state == sc.GestureState.ENDED:
                self.on_ended()

        # Public API:
        def on_began(self):
            pass

        def on_changed(self):
            pass

        def on_ended(self):
            pass

    class _ArcGesturePrioritize(sc.GestureManager):
        """
        Manager makes _ArcGesture the priority gesture
        """

        def can_be_prevented(self, gesture):
            # Never prevent in the middle of drag
            return gesture.state != sc.GestureState.CHANGED

        def should_prevent(self, gesture, preventer):
            if isinstance(preventer, SliderManipulator._ArcGesture):
                if preventer.state == sc.GestureState.BEGAN or preventer.state == sc.GestureState.CHANGED:
                    return True

    class _ArcGesture(sc.DragGesture):
        """
        Internal gesture that sets the new slider value and redirects to
        public SliderChangedGesture.
        """

        def __init__(self, manipulator):
            super().__init__(manager=SliderManipulator._ArcGesturePrioritize())
            self._manipulator = manipulator

        def __repr__(self):
            return f"<_ArcGesture at {hex(id(self))}>"

        def process(self):
            if self.state in [sc.GestureState.BEGAN, sc.GestureState.CHANGED, sc.GestureState.ENDED]:
                # Form new gesture_payload object
                new_gesture_payload = SliderManipulator.SliderDragGesturePayload(self.gesture_payload)
                # Save the new slider position in the gesture_payload object
                object_ray_point = self._manipulator.transform_space(
                    sc.Space.WORLD, sc.Space.OBJECT, self.gesture_payload.ray_closest_point
                )
                center = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("position"))
                slider_value = (object_ray_point[0] - center[0]) / self._manipulator.width + 0.5
                _min = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("min"))[0]
                _max = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("max"))[0]
                new_gesture_payload.slider_value = _min + slider_value * (_max - _min)
                # Call the public gesture
                self._manipulator._process_gesture(
                    SliderManipulator.SliderChangedGesture, self.state, new_gesture_payload
                )
            # Base process of the gesture
            super().process()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.width = 100
        self.thickness = 5
        self._radius = 5
        self._radius_hovered = 7

        def set_radius(circle, radius):
            circle.radius = radius

        # You don't recreate the gesture to make sure it's active when the
        # underlying object is recreated
        self._arc_gesture = self._ArcGesture(self)

        if hasattr(sc, "HoverGesture"):
            self._hover_gesture = sc.HoverGesture(
                on_began_fn=lambda sender: set_radius(sender, self._radius_hovered),
                on_ended_fn=lambda sender: set_radius(sender, self._radius),
            )
        else:
            self._hover_gesture = None

    def destroy(self):
        pass

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        self._width = value
        # Regenerate the mesh
        self.invalidate()

    @property
    def thickness(self):
        return self._thickness

    @thickness.setter
    def thickness(self, value):
        self._thickness = value
        # Regenerate the mesh
        self.invalidate()

    def on_build(self):
        """Called when the model is  changed and rebuilds the whole slider"""
        if not self.model:
            return

        # If you don't have a selection then just return
        if self.model.get_item("name") == "":
            return

        value = 0.0
        position = self.model.get_as_floats(self.model.get_item("position"))

        with sc.Transform(transform=sc.Matrix44.get_translation_matrix(*position)):

            # Left line
            line_from = -self.width * 0.5
            line_to = -self.width * 0.5 + self.width * 1 - self._radius
            if line_to > line_from:
                sc.Line([line_from, 0, 0], [line_to, 0, 0], color=cl.darkgray, thickness=self.thickness)

            # Circle
            circle_position = -self.width * 0.5 + self.width * 1
            with sc.Transform(transform=sc.Matrix44.get_translation_matrix(circle_position, 0, 0)):
                radius = self._radius
                sc.Arc(radius, axis=2, color=cl.gray)

        # Label
            with sc.Transform(look_at=sc.Transform.LookAt.CAMERA):
                with sc.Transform(scale_to=sc.Space.SCREEN):
                # Move it 5 points more to the top in the screen space
                    with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 5, 0)):
                        sc.Label(f"{value:.1f}", alignment=ui.Alignment.CENTER_BOTTOM)

    def on_model_updated(self, item):
        # Regenerate the manipulator
        self.invalidate()

Step 8.8: Update on_build()#

For your final step in the Manipulator module, you will update on_build() to update the min and max values of the model, update the line and circle, and update the label.

Start with replacing the value variable you had before with a new set of variables for _min,_max, new value, and value_normalized.

def on_build(self):
    """Called when the model is  changed and rebuilds the whole slider"""
    if not self.model:
        return

    # If you don't have a selection then just return
    if self.model.get_item("name") == "":
        return

 ### REPLACE ####
    value = 0.0

### WITH ####
    _min = self.model.get_as_floats(self.model.get_item("min"))[0]
    _max = self.model.get_as_floats(self.model.get_item("max"))[0]
    value = float(self.model.get_as_floats(self.model.get_item("value"))[0])
    value_normalized = (value - _min) / (_max - _min)
    value_normalized = max(min(value_normalized, 1.0), 0.0)
# END NEW

    position = self.model.get_as_floats(self.model.get_item("position"))

Now, you will add a new line to the slider so that you have a line for when the slider is moved to the left and to the right. Locate just below your previously set parameters the Left Line you created in Step 6.2.

Before you add the new line, replace the 1 in line_to with your new parameter value_normalized.

Then add the Right Line below the Left Line, as so:

with sc.Transform(transform=sc.Matrix44.get_translation_matrix(*position)):
    # Left line
    line_from = -self.width * 0.5
    line_to = -self.width * 0.5 + self.width * value_normalized - self._radius # REPLACED THE 1 WITH value_normalized
    if line_to > line_from:
        sc.Line([line_from, 0, 0], [line_to, 0, 0], color=cl.darkgray, thickness=self.thickness)

    # NEW: same as left line but flipped
    # Right line
    line_from = -self.width * 0.5 + self.width * value_normalized + self._radius
    line_to = self.width * 0.5
    if line_to > line_from:
        sc.Line([line_from, 0, 0], [line_to, 0, 0], color=cl.darkgray, thickness=self.thickness)
    # END NEW

Next, update the circle to add the hover_gesture. This will increase the circle in size when hovered over. Also change the 1 value like you did for Line to value_normalized and also add the gesture to sc.Arc:

# Circle
# NEW : Changed 1 value to value_normalized
circle_position = -self.width * 0.5 + self.width * value_normalized
with sc.Transform(transform=sc.Matrix44.get_translation_matrix(circle_position, 0, 0)):
    radius = self._radius
    # NEW: Added Gesture when hovering over the circle it will increase in size
    gestures = [self._arc_gesture]
    if self._hover_gesture:
        gestures.append(self._hover_gesture)

        if self._hover_gesture.state == sc.GestureState.CHANGED:
            radius = self._radius_hovered

    sc.Arc(radius, axis=2, color=cl.gray, gestures=gestures)
     # END NEW

Last of all, update the Label below your circle to add more space between the slider and the label:

with sc.Transform(look_at=sc.Transform.LookAt.CAMERA):
# NEW: Added more space between the slider and the label
# Move it to the top
    with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, self._radius_hovered, 0)):
# END NEW
        with sc.Transform(scale_to=sc.Space.SCREEN):
    # Move it 5 points more to the top in the screen space
            with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 5, 0)):
                sc.Label(f"{value:.1f}", alignment=ui.Alignment.CENTER_BOTTOM)
Click here for the end code of slider_manipulator.py
from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui

class SliderManipulator(sc.Manipulator):

    class SliderDragGesturePayload(sc.AbstractGesture.GesturePayload):
        """
        Public payload. The user will access it to get the current value of
        the slider.
        """

        def __init__(self, base):
            super().__init__(base.item_closest_point, base.ray_closest_point, base.ray_distance)
            self.slider_value = 0

    class SliderChangedGesture(sc.ManipulatorGesture):
        """
        Public Gesture. The user will reimplement it to process the
        manipulator's callbacks.
        """

        def __init__(self, **kwargs):
            super().__init__(**kwargs)

        def process(self):
            # Redirection to methods
            if self.state == sc.GestureState.BEGAN:
                self.on_began()
            elif self.state == sc.GestureState.CHANGED:
                self.on_changed()
            elif self.state == sc.GestureState.ENDED:
                self.on_ended()

        # Public API:
        def on_began(self):
            pass

        def on_changed(self):
            pass

        def on_ended(self):
            pass

    class _ArcGesturePrioritize(sc.GestureManager):
        """
        Manager makes _ArcGesture the priority gesture
        """

        def can_be_prevented(self, gesture):
            # Never prevent in the middle of drag
            return gesture.state != sc.GestureState.CHANGED

        def should_prevent(self, gesture, preventer):
            if isinstance(preventer, SliderManipulator._ArcGesture):
                if preventer.state == sc.GestureState.BEGAN or preventer.state == sc.GestureState.CHANGED:
                    return True

    class _ArcGesture(sc.DragGesture):
        """
        Internal gesture that sets the new slider value and redirects to
        public SliderChangedGesture.
        """

        def __init__(self, manipulator):
            super().__init__(manager=SliderManipulator._ArcGesturePrioritize())
            self._manipulator = manipulator

        def __repr__(self):
            return f"<_ArcGesture at {hex(id(self))}>"

        def process(self):
            if self.state in [sc.GestureState.BEGAN, sc.GestureState.CHANGED, sc.GestureState.ENDED]:
                # Form new gesture_payload object
                new_gesture_payload = SliderManipulator.SliderDragGesturePayload(self.gesture_payload)
                # Save the new slider position in the gesture_payload object
                object_ray_point = self._manipulator.transform_space(
                    sc.Space.WORLD, sc.Space.OBJECT, self.gesture_payload.ray_closest_point
                )
                center = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("position"))
                slider_value = (object_ray_point[0] - center[0]) / self._manipulator.width + 0.5
                _min = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("min"))[0]
                _max = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("max"))[0]
                new_gesture_payload.slider_value = _min + slider_value * (_max - _min)
                # Call the public gesture
                self._manipulator._process_gesture(
                    SliderManipulator.SliderChangedGesture, self.state, new_gesture_payload
                )
            # Base process of the gesture
            super().process()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.width = 100
        self.thickness = 5
        self._radius = 5
        self._radius_hovered = 7

        def set_radius(circle, radius):
            circle.radius = radius

        # You don't recreate the gesture to make sure it's active when the
        # underlying object is recreated
        self._arc_gesture = self._ArcGesture(self)

        if hasattr(sc, "HoverGesture"):
            self._hover_gesture = sc.HoverGesture(
                on_began_fn=lambda sender: set_radius(sender, self._radius_hovered),
                on_ended_fn=lambda sender: set_radius(sender, self._radius),
            )
        else:
            self._hover_gesture = None

    def destroy(self):
        pass

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        self._width = value
        # Regenerate the mesh
        self.invalidate()

    @property
    def thickness(self):
        return self._thickness

    @thickness.setter
    def thickness(self, value):
        self._thickness = value
        # Regenerate the mesh
        self.invalidate()

    def on_build(self):
        """Called when the model is  changed and rebuilds the whole slider"""
        if not self.model:
            return

        # If you don't have a selection then just return
        if self.model.get_item("name") == "":
            return

        _min = self.model.get_as_floats(self.model.get_item("min"))[0]
        _max = self.model.get_as_floats(self.model.get_item("max"))[0]
        value = float(self.model.get_as_floats(self.model.get_item("value"))[0])
        value_normalized = (value - _min) / (_max - _min)
        value_normalized = max(min(value_normalized, 1.0), 0.0)
        position = self.model.get_as_floats(self.model.get_item("position"))

        with sc.Transform(transform=sc.Matrix44.get_translation_matrix(*position)):

            # Left line
            line_from = -self.width * 0.5
            line_to = -self.width * 0.5 + self.width * value_normalized - self._radius # REPLACED THE 1 WITH value_normalized
            if line_to > line_from:
                sc.Line([line_from, 0, 0], [line_to, 0, 0], color=cl.darkgray, thickness=self.thickness)

            # NEW: same as left line but flipped
            # Right line
            line_from = -self.width * 0.5 + self.width * value_normalized + self._radius
            line_to = self.width * 0.5
            if line_to > line_from:
                sc.Line([line_from, 0, 0], [line_to, 0, 0], color=cl.darkgray, thickness=self.thickness)


            # Circle
            circle_position = -self.width * 0.5 + self.width * value_normalized
            with sc.Transform(transform=sc.Matrix44.get_translation_matrix(circle_position, 0, 0)):
                radius = self._radius
                gestures = [self._arc_gesture]
                if self._hover_gesture:
                    gestures.append(self._hover_gesture)

                    if self._hover_gesture.state == sc.GestureState.CHANGED:
                        radius = self._radius_hovered
                sc.Arc(radius, axis=2, color=cl.gray)

        # Label
            with sc.Transform(look_at=sc.Transform.LookAt.CAMERA):
            # NEW: Added more space between the slider and the label
            # Move it to the top
                with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, self._radius_hovered, 0)):
            # END NEW
                    with sc.Transform(scale_to=sc.Space.SCREEN):
                # Move it 5 points more to the top in the screen space
                        with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 5, 0)):
                            sc.Label(f"{value:.1f}", alignment=ui.Alignment.CENTER_BOTTOM)

    def on_model_updated(self, item):
        # Regenerate the manipulator
        self.invalidate()

Note

If you are running into any errors in the Console, disable Autoload in the Extension Manager and restart Omniverse Code.

Congratulations!#

You have completed the guide How to make a Slider Manipulator and now have a working scale slider!

../_images/work_viewportUtilOn.png