Undo/Redo demo

move cubes and rewind with Ctrl+Z / Ctrl+Shift+Z.

▶ Run in browser

Tags: ui

Run with: uv run python examples/features/ui/undo.py

Controls: 1 / 2 / 3 Select cube Arrow keys Move selected cube (Left/Right = X, Up/Down = Z) Ctrl+Z Undo last move Ctrl+Shift+Z / Ctrl+Y Redo

Source

  1"""Undo/Redo demo -- move cubes and rewind with Ctrl+Z / Ctrl+Shift+Z.
  2
  3Run with:
  4    uv run python examples/features/ui/undo.py
  5
  6Controls:
  7    1 / 2 / 3           Select cube
  8    Arrow keys           Move selected cube (Left/Right = X, Up/Down = Z)
  9    Ctrl+Z               Undo last move
 10    Ctrl+Shift+Z / Ctrl+Y  Redo
 11"""
 12
 13from simvx.core import (
 14    Camera3D,
 15    Input,
 16    Key,
 17    Material,
 18    Mesh,
 19    MeshInstance3D,
 20    Node,
 21    PropertyCommand,
 22    Selection,
 23    UndoStack,
 24    Vec3,
 25)
 26from simvx.graphics import App
 27
 28MOVE_STEP = 1.0
 29COOLDOWN = 0.15  # seconds between accepted key repeats
 30
 31CUBE_COLOURS = [
 32    (0.9, 0.2, 0.2, 1.0),  # red
 33    (0.2, 0.8, 0.2, 1.0),  # green
 34    (0.2, 0.4, 0.9, 1.0),  # blue
 35]
 36CUBE_POSITIONS = [
 37    Vec3(-4, 0, 0),
 38    Vec3(0, 0, 0),
 39    Vec3(4, 0, 0),
 40]
 41
 42
 43class UndoDemo(Node):
 44    def on_ready(self):
 45        # Camera looking down Y axis
 46        cam = Camera3D(position=(0, -15, 0))
 47        cam.look_at((0, 0, 0), up=(0, 0, 1))
 48        self.add_child(cam)
 49
 50        # Shared mesh
 51        cube_mesh = Mesh.cube()
 52
 53        # Create cubes
 54        self.cubes = []
 55        for i in range(3):
 56            cube = MeshInstance3D(
 57                name=f"Cube{i + 1}",
 58                mesh=cube_mesh,
 59                material=Material(colour=CUBE_COLOURS[i]),
 60                position=tuple(CUBE_POSITIONS[i]),
 61            )
 62            self.add_child(cube)
 63            self.cubes.append(cube)
 64
 65        # Undo system
 66        self.undo_stack = UndoStack()
 67        self.undo_stack.changed.connect(self._on_stack_changed)
 68
 69        # Selection
 70        self.selection = Selection()
 71        self.selection.selection_changed.connect(self._on_selection_changed)
 72        self.selection.select(self.cubes[0])
 73
 74        self._cooldown_timer = 0.0
 75
 76    def on_process(self, dt):
 77        self._cooldown_timer = max(0.0, self._cooldown_timer - dt)
 78
 79        # Cube selection (1-3)
 80        for i, key in enumerate((Key.KEY_1, Key.KEY_2, Key.KEY_3)):
 81            if Input.is_key_just_pressed(key):
 82                self.selection.select(self.cubes[i])
 83
 84        ctrl = Input.is_key_pressed(Key.LEFT_CONTROL) or Input.is_key_pressed(Key.RIGHT_CONTROL)
 85        shift = Input.is_key_pressed(Key.LEFT_SHIFT) or Input.is_key_pressed(Key.RIGHT_SHIFT)
 86
 87        # Undo / Redo (react on just-pressed only)
 88        if ctrl and Input.is_key_just_pressed(Key.Z):
 89            if shift:
 90                self.undo_stack.redo()
 91            else:
 92                self.undo_stack.undo()
 93            return
 94        if ctrl and Input.is_key_just_pressed(Key.Y):
 95            self.undo_stack.redo()
 96            return
 97
 98        # Movement (with cooldown for key repeat)
 99        if self._cooldown_timer > 0 or self.selection.empty:
100            return
101
102        direction = Vec3(0, 0, 0)
103        if Input.is_key_pressed(Key.LEFT):
104            direction = Vec3(-MOVE_STEP, 0, 0)
105        elif Input.is_key_pressed(Key.RIGHT):
106            direction = Vec3(MOVE_STEP, 0, 0)
107        elif Input.is_key_pressed(Key.UP):
108            direction = Vec3(0, 0, MOVE_STEP)
109        elif Input.is_key_pressed(Key.DOWN):
110            direction = Vec3(0, 0, -MOVE_STEP)
111
112        if direction.length() > 0:
113            cube = self.selection.primary
114            old_pos = tuple(Vec3(cube.position))
115            new_pos = tuple(Vec3(cube.position) + direction)
116            cmd = PropertyCommand(
117                cube,
118                "position",
119                old_pos,
120                new_pos,
121                description=f"Move {cube.name} to ({new_pos[0]:.0f}, {new_pos[2]:.0f})",
122            )
123            self.undo_stack.push(cmd)
124            self._cooldown_timer = COOLDOWN
125
126    def _on_selection_changed(self):
127        pass
128
129    def _on_stack_changed(self):
130        pass
131
132
133if __name__ == "__main__":
134    app = App(title="SimVX Undo Demo", width=800, height=600)
135    app.run(UndoDemo())