1use 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 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 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 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 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 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 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 for e in &existing {
169 commands.entity(e).remove::<TransformGizmoFocus>();
170 }
171 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}