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//! - Radio buttons switch between Translate/Rotate/Scale modes and toggle World/Local space.
6
7use bevy::{
8    camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},
9    feathers::{controls::FeathersSlider, display::label, theme::UiTheme, FeathersPlugins},
10    gizmos::transform_gizmo::{
11        TransformGizmoCamera, TransformGizmoFocus, TransformGizmoMode, TransformGizmoPlugin,
12        TransformGizmoSettings, TransformGizmoSpace,
13    },
14    picking::{pointer::PointerButton, Pickable},
15    prelude::*,
16    ui_widgets::{radio_self_update, SliderPrecision, SliderStep, SliderValue, ValueChange},
17};
18
19use radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
20
21#[path = "../helpers/radio.rs"]
22mod radio;
23
24#[path = "../helpers/theme.rs"]
25mod theme;
26
27fn main() {
28    App::new()
29        .add_plugins((
30            DefaultPlugins,
31            FreeCameraPlugin,
32            MeshPickingPlugin,
33            TransformGizmoPlugin,
34            FeathersPlugins,
35        ))
36        .insert_resource(UiTheme(theme::basic_example_theme(Color::WHITE)))
37        .add_systems(Startup, setup)
38        .add_systems(Update, toggle_scale_slider)
39        .add_observer(update_radio_button)
40        .add_observer(radio_self_update)
41        .run();
42}
43
44fn setup(
45    mut commands: Commands,
46    mut meshes: ResMut<Assets<Mesh>>,
47    mut materials: ResMut<Assets<StandardMaterial>>,
48) {
49    commands.spawn_scene(bsn! {
50        @main_ui_node_scene()
51        Children [
52            @label("Click an object to select it")
53            --
54            @feathers_option_buttons("",
55            &[
56                (TransformGizmoMode::Translate, "Translate"),
57                (TransformGizmoMode::Rotate, "Rotate"),
58                (TransformGizmoMode::Scale, "Scale"),
59            ], 0)
60            --
61            @feathers_option_buttons("",
62            &[
63                (TransformGizmoSpace::World, "World"),
64                (TransformGizmoSpace::Local, "Local"),
65            ], 0)
66            --
67            Node {
68                align_items: AlignItems::Center,
69                column_gap: px(4)
70            }
71            ScaleSensitivitySlider
72            Children [
73                @label("Sensitivity")
74                --
75                @FeathersSlider {
76                    @max: 2.0,
77                    @min: 0.1,
78                }
79                SliderValue(1.0)
80                SliderPrecision(2)
81                SliderStep(0.1)
82                on(slider_update)
83            ]
84        ]
85    });
86
87    // Ground plane (not pickable)
88    commands.spawn((
89        Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))),
90        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.3, 0.3))),
91        Pickable::IGNORE,
92    ));
93
94    // Table: a parent body with a child part, demonstrating local vs world space.
95    // The parent cube is selected by default.
96    commands
97        .spawn((
98            Mesh3d(meshes.add(Cuboid::new(1.5, 0.15, 1.0))),
99            MeshMaterial3d(materials.add(Color::srgb(0.8, 0.3, 0.3))),
100            Transform::from_xyz(-2.0, 1.0, 0.0),
101            TransformGizmoFocus,
102        ))
103        .observe(on_click_select)
104        .with_children(|parent| {
105            // Table leg (child)
106            parent.spawn((
107                Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))),
108                MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))),
109                Transform::from_xyz(-0.6, -0.5, 0.4),
110                Pickable::IGNORE,
111            ));
112            parent.spawn((
113                Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))),
114                MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))),
115                Transform::from_xyz(0.6, -0.5, 0.4),
116                Pickable::IGNORE,
117            ));
118            parent.spawn((
119                Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))),
120                MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))),
121                Transform::from_xyz(-0.6, -0.5, -0.4),
122                Pickable::IGNORE,
123            ));
124            parent.spawn((
125                Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))),
126                MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))),
127                Transform::from_xyz(0.6, -0.5, -0.4),
128                Pickable::IGNORE,
129            ));
130        });
131
132    // Standalone cube
133    commands
134        .spawn((
135            Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
136            MeshMaterial3d(materials.add(Color::srgb(0.3, 0.8, 0.3))),
137            Transform::from_xyz(2.0, 0.5, 0.0),
138        ))
139        .observe(on_click_select);
140
141    // Light
142    commands.spawn((
143        DirectionalLight {
144            shadow_maps_enabled: true,
145            ..default()
146        },
147        Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -0.8, 0.4, 0.0)),
148    ));
149
150    // Camera
151    commands.spawn((
152        Camera3d::default(),
153        Transform::from_xyz(0.0, 4.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
154        FreeCamera::default(),
155        TransformGizmoCamera,
156    ));
157}
158
159fn on_click_select(
160    click: On<PointerClick>,
161    mut commands: Commands,
162    existing: Query<Entity, With<TransformGizmoFocus>>,
163) {
164    if click.button != PointerButton::Primary {
165        return;
166    }
167    // Remove focus from all entities
168    for e in &existing {
169        commands.entity(e).remove::<TransformGizmoFocus>();
170    }
171    // Add focus to clicked entity
172    commands.entity(click.entity).insert(TransformGizmoFocus);
173}
174
175fn update_radio_button(
176    event: On<ValueChange<Entity>>,
177    mode_value_query: Query<&RadioButtonOptionValue<TransformGizmoMode>>,
178    space_value_query: Query<&RadioButtonOptionValue<TransformGizmoSpace>>,
179    mut settings: ResMut<TransformGizmoSettings>,
180) {
181    if let Ok(RadioButtonOptionValue(mode)) = mode_value_query.get(event.value) {
182        settings.mode = *mode;
183    } else if let Ok(RadioButtonOptionValue(space)) = space_value_query.get(event.value) {
184        settings.space = *space;
185    };
186}
187
188fn slider_update(
189    value_change: On<ValueChange<f32>>,
190    mut commands: Commands,
191    mut settings: ResMut<TransformGizmoSettings>,
192) {
193    commands
194        .entity(value_change.source)
195        .insert(SliderValue(value_change.value));
196
197    settings.scale_sensitivity = value_change.value;
198}
199
200#[derive(Component, Clone, Default)]
201struct ScaleSensitivitySlider;
202
203fn toggle_scale_slider(
204    settings: Res<TransformGizmoSettings>,
205    mut slider_node: Single<&mut Node, With<ScaleSensitivitySlider>>,
206) {
207    slider_node.display = if settings.mode == TransformGizmoMode::Scale {
208        Display::Flex
209    } else {
210        Display::None
211    };
212}