Mouse picking demo

click cubes to change their colour.

▶ Run in browser

Tags: 3d

Uses the engine’s built-in object picking: give a node a CollisionShape3D with pickable=True and override on_picked. Each click casts a ray (SceneTree.input_cast) and the nearest pickable collider’s owner receives on_picked. For the manual approach (casting a world-space ray with screen_to_ray and querying self.physics.raycast yourself) see raycast.py.

Usage: uv run python examples/features/3d/picking.py

Source

 1"""Mouse picking demo: click cubes to change their colour.
 2
 3Uses the engine's built-in object picking: give a node a ``CollisionShape3D``
 4with ``pickable=True`` and override ``on_picked``. Each click casts a ray
 5(``SceneTree.input_cast``) and the nearest pickable collider's owner receives
 6``on_picked``. For the manual approach (casting a world-space ray with
 7``screen_to_ray`` and querying ``self.physics.raycast`` yourself) see ``raycast.py``.
 8
 9Usage:
10    uv run python examples/features/3d/picking.py
11"""
12
13from simvx.core import (
14    Camera3D,
15    CollisionShape3D,
16    Material,
17    Mesh,
18    MeshInstance3D,
19    Node,
20    SphereShape3D,
21    Text2D,
22)
23from simvx.graphics import App
24
25
26class PickableCube(MeshInstance3D):
27    """A cube that responds to mouse clicks."""
28
29    _COLOURS = [
30        (0.9, 0.1, 0.1, 1),
31        (0.1, 0.9, 0.1, 1),
32        (0.1, 0.1, 0.9, 1),
33        (0.9, 0.9, 0.1, 1),
34        (0.9, 0.1, 0.9, 1),
35        (0.1, 0.9, 0.9, 1),
36    ]
37
38    def __init__(self, label="Cube", colour_idx=0, **kwargs):
39        mat = Material(colour=self._COLOURS[colour_idx % len(self._COLOURS)])
40        super().__init__(material=mat, **kwargs)
41        self.label = label
42        self._colour_idx = colour_idx
43
44    def on_ready(self):
45        # Inscribed sphere (touches face centers) for tight picking
46        shape = CollisionShape3D(shape=SphereShape3D(radius=0.5))
47        shape.pickable = True
48        self.add_child(shape)
49
50    def on_picked(self, event):
51        """Called when this node is picked by mouse ray."""
52        self._colour_idx = (self._colour_idx + 1) % len(self._COLOURS)
53        new_colour = self._COLOURS[self._colour_idx]
54        self.material.colour = new_colour
55        print(f"PICKED {self.label} -> colour {self._colour_idx}", flush=True)
56
57
58class PickingScene(Node):
59    def on_ready(self):
60        # Camera: looking down Y axis
61        cam = Camera3D(position=(0, -15, 0))
62        cam.look_at((0, 0, 0), up=(0, 0, 1))
63        self.add_child(cam)
64
65        # Shared mesh
66        cube_mesh = Mesh.cube()
67
68        # Grid of pickable cubes
69        positions = [
70            (-3, 0, -2),
71            (0, 0, -2),
72            (3, 0, -2),
73            (-3, 0, 2),
74            (0, 0, 2),
75            (3, 0, 2),
76        ]
77        for i, pos in enumerate(positions):
78            cube = PickableCube(
79                label=f"Cube_{i}",
80                colour_idx=i,
81                mesh=cube_mesh,
82                position=pos,
83            )
84            self.add_child(cube)
85
86        # HUD
87        self.add_child(Text2D(text="Click cubes to change colour", position=(10, 10), font_scale=1.5))
88
89    def on_update(self, dt):
90        pass
91
92
93if __name__ == "__main__":
94    app = App(title="Picking Demo", width=1280, height=720)
95    app.run(PickingScene())