docs/bevy/examples/gizmos/transform_gizmo.rs 6.7 K · 212 lines · raw · history

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
7 use 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
19 use radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
20
21 #[path = "../helpers/radio.rs"]
22 mod radio;
23
24 #[path = "../helpers/theme.rs"]
25 mod theme;
26
27 fn 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
44 fn 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
159 fn 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
175 fn 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
188 fn 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)]
201 struct ScaleSensitivitySlider;
202
203 fn 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 }