Skip to content

Latest commit

 

History

History
110 lines (72 loc) · 2.84 KB

File metadata and controls

110 lines (72 loc) · 2.84 KB

Implementing Commands

All user-facing mutations must go through omni.kit.commands.Command. This is what makes actions undoable. Any action that modifies data and is visible to the user requires this pattern — direct mutations will be rejected in review.


When to Use This Pattern

Whenever a user action changes data: setting a property, moving a prim, replacing an asset, deleting an element, etc. If the action would have an "Undo" entry in a standard application, it needs a Command here.


Class Template

Place the command in commands.py in the relevant .core extension and export it from __init__.py.

__all__ = ["MyActionCommand"]

import omni.kit.commands
import omni.kit.undo


class MyActionCommand(omni.kit.commands.Command):
    """One-line description of what this command does."""

    def __init__(self, param: str):
        self._param = param
        self._previous_value: str | None = None  # captured in do(), used in undo()

    def do(self):
        self._previous_value = get_current_value()  # capture state BEFORE modifying
        set_value(self._param)

    def undo(self):
        set_value(self._previous_value)

Critical rules:

  • do() must capture all state needed for undo() before making any changes.
  • Never call omni.kit.commands.execute() from inside do() or undo() — this corrupts the undo stack.
  • Never instantiate a command and call do() directly — that bypasses the undo stack entirely.

Executing a Command

omni.kit.commands.execute("MyActionCommand", param="value")

The string name must exactly match the class name. Commands are registered globally by name — using the wrong string fails silently.


Grouping Multiple Sub-Commands

When a user action logically requires several sub-commands that should undo as one unit:

with omni.kit.undo.group():
    omni.kit.commands.execute("SubCommandA", ...)
    omni.kit.commands.execute("SubCommandB", ...)

Extension Dependency

Declare explicitly if this is the first extension in the dependency chain to use omni.kit.commands:

[dependencies]
"omni.kit.commands" = {}

Testing

Write two separate tests — one Act each. The undo test's execute() call belongs in Arrange (it is setup, not the thing under test).

async def test_my_action_command_applies_new_value(self):
    # Arrange
    set_value("old_value")

    # Act
    omni.kit.commands.execute("MyActionCommand", param="new_value")

    # Assert
    self.assertEqual(get_current_value(), "new_value")


async def test_my_action_command_undo_restores_previous_value(self):
    # Arrange
    set_value("original_value")
    omni.kit.commands.execute("MyActionCommand", param="new_value")

    # Act
    omni.kit.undo.undo()

    # Assert
    self.assertEqual(get_current_value(), "original_value")