Skip to main content

transform_gizmo/
transform_gizmo.rs

1//! Interactive transform gizmo example.
2//!
3//! Demonstrates translate, rotate, and scale gizmos with click-to-select.
4//! - Click an object to select it (primary mouse button)
5//! - **1** = Translate, **2** = Rotate, **3** = Scale
6//! - **X** = Toggle World/Local space
7
8use bevy::{
9    camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},
10    gizmos::transform_gizmo::{
11        TransformGizmoCamera, TransformGizmoFocus, TransformGizmoMode, TransformGizmoPlugin,
12        TransformGizmoSettings, TransformGizmoSpace,
13    },
14    picking::{pointer::PointerButton, Pickable},
15    prelude::*,
16};
17
18fn main() {
19    App::new()
20        .add_plugins((
21            DefaultPlugins,
22            FreeCameraPlugin,
23            MeshPickingPlugin,
24            TransformGizmoPlugin,
25        ))
26        .add_systems(Startup, setup)
27        .add_systems(Update, (gizmo_mode_keys, update_instructions))
28        .run();
29}
30
31fn setup(
32    mut commands: Commands,
33    mut meshes: ResMut<Assets<Mesh>>,
34    mut materials: ResMut<Assets<StandardMaterial>>,
35) {
36    // Instructions
37    commands.spawn((
38        Text::new(
39            "Click an object to select it\n1: Translate | 2: Rotate | 3: Scale | X: World/Local space",
40        ),
41        Node {
42            position_type: PositionType::Absolute,
43            top: px(12),
44            left: px(12),
45            ..default()
46        },
47        InstructionsText,
48    ));
49
50    // Ground plane (not pickable)
51    commands.spawn((
52        Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))),
53        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.3, 0.3))),
54        Pickable::IGNORE,
55    ));
56
57    // Table: a parent body with a child part, demonstrating local vs world space.
58    // The parent cube is selected by default.
59    commands
60        .spawn((
61            Mesh3d(meshes.add(Cuboid::new(1.5, 0.15, 1.0))),
62            MeshMaterial3d(materials.add(Color::srgb(0.8, 0.3, 0.3))),
63            Transform::from_xyz(-2.0, 1.0, 0.0),
64            TransformGizmoFocus,
65        ))
66        .observe(on_click_select)
67        .with_children(|parent| {
68            // Table leg (child)
69            parent.spawn((
70                Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))),
71                MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))),
72                Transform::from_xyz(-0.6, -0.5, 0.4),
73                Pickable::IGNORE,
74            ));
75            parent.spawn((
76                Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))),
77                MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))),
78                Transform::from_xyz(0.6, -0.5, 0.4),
79                Pickable::IGNORE,
80            ));
81            parent.spawn((
82                Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))),
83                MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))),
84                Transform::from_xyz(-0.6, -0.5, -0.4),
85                Pickable::IGNORE,
86            ));
87            parent.spawn((
88                Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))),
89                MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))),
90                Transform::from_xyz(0.6, -0.5, -0.4),
91                Pickable::IGNORE,
92            ));
93        });
94
95    // Standalone cube
96    commands
97        .spawn((
98            Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
99            MeshMaterial3d(materials.add(Color::srgb(0.3, 0.8, 0.3))),
100            Transform::from_xyz(2.0, 0.5, 0.0),
101        ))
102        .observe(on_click_select);
103
104    // Light
105    commands.spawn((
106        DirectionalLight {
107            shadow_maps_enabled: true,
108            ..default()
109        },
110        Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -0.8, 0.4, 0.0)),
111    ));
112
113    // Camera
114    commands.spawn((
115        Camera3d::default(),
116        Transform::from_xyz(0.0, 4.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
117        FreeCamera::default(),
118        TransformGizmoCamera,
119    ));
120}
121
122fn on_click_select(
123    click: On<Pointer<Click>>,
124    mut commands: Commands,
125    existing: Query<Entity, With<TransformGizmoFocus>>,
126) {
127    if click.button != PointerButton::Primary {
128        return;
129    }
130    // Remove focus from all entities
131    for e in &existing {
132        commands.entity(e).remove::<TransformGizmoFocus>();
133    }
134    // Add focus to clicked entity
135    commands.entity(click.entity).insert(TransformGizmoFocus);
136}
137
138// Note: Using 1/2/3 instead of Blender's G/R/S because S conflicts with
139// the FreeCameraPlugin's WASD movement controls.
140fn gizmo_mode_keys(
141    keyboard: Res<ButtonInput<KeyCode>>,
142    mut settings: ResMut<TransformGizmoSettings>,
143) {
144    if keyboard.just_pressed(KeyCode::Digit1) {
145        settings.mode = TransformGizmoMode::Translate;
146    }
147    if keyboard.just_pressed(KeyCode::Digit2) {
148        settings.mode = TransformGizmoMode::Rotate;
149    }
150    if keyboard.just_pressed(KeyCode::Digit3) {
151        settings.mode = TransformGizmoMode::Scale;
152    }
153    if keyboard.just_pressed(KeyCode::KeyX) {
154        settings.space = match settings.space {
155            TransformGizmoSpace::World => TransformGizmoSpace::Local,
156            TransformGizmoSpace::Local => TransformGizmoSpace::World,
157        };
158    }
159}
160
161#[derive(Component)]
162struct InstructionsText;
163
164fn update_instructions(
165    settings: Res<TransformGizmoSettings>,
166    mut text: Single<&mut Text, With<InstructionsText>>,
167) {
168    let mode_str = match settings.mode {
169        TransformGizmoMode::Translate => "Translate",
170        TransformGizmoMode::Rotate => "Rotate",
171        TransformGizmoMode::Scale => "Scale",
172    };
173    let space_str = match settings.space {
174        TransformGizmoSpace::World => "World",
175        TransformGizmoSpace::Local => "Local",
176    };
177    text.0 = format!(
178        "Click an object to select it\n1: Translate | 2: Rotate | 3: Scale | X: World/Local space\nMode: {mode_str} | Space: {space_str}"
179    );
180}