1use bevy_app::prelude::*;
32use bevy_asset::{AssetApp, Assets};
33use bevy_ecs::prelude::*;
34use bevy_input::prelude::*;
35use bevy_math::{DQuat, DVec3, Vec2};
36use bevy_picking::hover::HoverMap;
37use bevy_platform::collections::HashMap;
38use bevy_render::prelude::*;
39use bevy_transform::prelude::*;
40use bevy_window::{PrimaryWindow, Window};
41use mouse_interact::MouseGizmoInteractionPlugin;
42use picking::TransformGizmoPickingPlugin;
43use uuid::Uuid;
44
45use render::{DrawDataHandles, TransformGizmoRenderPlugin};
46use transform_gizmo::config::{
47 DEFAULT_SNAP_ANGLE, DEFAULT_SNAP_DISTANCE, DEFAULT_SNAP_SCALE, GizmoModeKind,
48 TransformPivotPoint,
49};
50pub use transform_gizmo::{
51 GizmoConfig,
52 math::{Pos2, Rect},
53 *,
54};
55
56pub mod mouse_interact;
57pub mod picking;
58pub mod prelude;
59
60mod render;
61
62const GIZMO_GROUP_UUID: Uuid = Uuid::from_u128(0x_1c90_3d44_0152_45e1_b1c9_889a_0203_e90c);
63
64pub struct TransformGizmoPlugin;
69
70impl Plugin for TransformGizmoPlugin {
71 fn build(&self, app: &mut App) {
72 app.init_asset::<render::GizmoDrawData>()
73 .init_resource::<GizmoOptions>()
74 .init_resource::<GizmoStorage>()
75 .add_event::<GizmoDragStarted>()
76 .add_event::<GizmoDragging>()
77 .add_plugins(TransformGizmoRenderPlugin)
78 .add_systems(
79 Last,
80 (handle_hotkeys, update_gizmos, draw_gizmos, cleanup_old_data).chain(),
81 );
82
83 #[cfg(feature = "gizmo_picking_backend")]
84 app.add_plugins(TransformGizmoPickingPlugin);
85 #[cfg(feature = "mouse_interaction")]
86 app.add_plugins(MouseGizmoInteractionPlugin);
87 }
88}
89
90#[derive(Resource, Copy, Clone, Debug)]
92pub struct GizmoOptions {
93 pub gizmo_modes: EnumSet<GizmoMode>,
95 pub gizmo_orientation: GizmoOrientation,
97 pub pivot_point: TransformPivotPoint,
99 pub visuals: GizmoVisuals,
101 pub snapping: bool,
104 pub accurate_mode: bool,
107 pub snap_angle: f32,
109 pub snap_distance: f32,
111 pub snap_scale: f32,
113 pub group_targets: bool,
117 pub mode_override: Option<GizmoMode>,
120 pub hotkeys: Option<GizmoHotkeys>,
122 pub viewport_rect: Option<bevy_math::Rect>,
126}
127
128impl Default for GizmoOptions {
129 fn default() -> Self {
130 Self {
131 gizmo_modes: GizmoMode::all(),
132 gizmo_orientation: GizmoOrientation::default(),
133 pivot_point: TransformPivotPoint::default(),
134 visuals: Default::default(),
135 snapping: false,
136 accurate_mode: false,
137 snap_angle: DEFAULT_SNAP_ANGLE,
138 snap_distance: DEFAULT_SNAP_DISTANCE,
139 snap_scale: DEFAULT_SNAP_SCALE,
140 group_targets: true,
141 mode_override: None,
142 hotkeys: None,
143 viewport_rect: None,
144 }
145 }
146}
147
148#[derive(Debug, Copy, Clone)]
150pub struct GizmoHotkeys {
151 pub enable_snapping: Option<KeyCode>,
154 pub enable_accurate_mode: Option<KeyCode>,
156 pub toggle_rotate: Option<KeyCode>,
158 pub toggle_translate: Option<KeyCode>,
160 pub toggle_scale: Option<KeyCode>,
162 pub toggle_x: Option<KeyCode>,
164 pub toggle_y: Option<KeyCode>,
166 pub toggle_z: Option<KeyCode>,
168 pub deactivate_gizmo: Option<KeyCode>,
171 pub mouse_click_deactivates: bool,
174}
175
176impl Default for GizmoHotkeys {
177 fn default() -> Self {
178 Self {
179 enable_snapping: Some(KeyCode::ControlLeft),
180 enable_accurate_mode: Some(KeyCode::ShiftLeft),
181 toggle_rotate: Some(KeyCode::KeyR),
182 toggle_translate: Some(KeyCode::KeyG),
183 toggle_scale: Some(KeyCode::KeyS),
184 toggle_x: Some(KeyCode::KeyX),
185 toggle_y: Some(KeyCode::KeyY),
186 toggle_z: Some(KeyCode::KeyZ),
187 deactivate_gizmo: Some(KeyCode::Escape),
188 mouse_click_deactivates: true,
189 }
190 }
191}
192
193#[derive(Component, Copy, Clone, Debug, Default)]
203pub struct GizmoTarget {
204 pub(crate) is_focused: bool,
206
207 pub(crate) is_active: bool,
209
210 pub(crate) latest_result: Option<GizmoResult>,
213}
214
215impl GizmoTarget {
216 pub fn is_focused(&self) -> bool {
218 self.is_focused
219 }
220
221 pub fn is_active(&self) -> bool {
223 self.is_active
224 }
225
226 pub fn latest_result(&self) -> Option<GizmoResult> {
229 self.latest_result
230 }
231}
232
233#[derive(Component)]
235pub struct GizmoCamera;
236
237#[derive(Resource, Default)]
238struct GizmoStorage {
239 target_entities: Vec<Entity>,
240 entity_gizmo_map: HashMap<Entity, Uuid>,
241 gizmos: HashMap<Uuid, Gizmo>,
242}
243
244fn handle_hotkeys(
245 mut gizmo_options: ResMut<GizmoOptions>,
246 keyboard_input: Res<ButtonInput<KeyCode>>,
247 mouse_input: Res<ButtonInput<MouseButton>>,
248 mut axes: Local<EnumSet<GizmoDirection>>,
249) {
250 let Some(hotkeys) = gizmo_options.hotkeys else {
251 return;
253 };
254
255 if let Some(snapping_key) = hotkeys.enable_snapping {
256 gizmo_options.snapping = keyboard_input.pressed(snapping_key);
257 }
258
259 if let Some(accurate_mode_key) = hotkeys.enable_accurate_mode {
260 gizmo_options.accurate_mode = keyboard_input.pressed(accurate_mode_key);
261 }
262
263 let invert_modifier = keyboard_input.pressed(KeyCode::ShiftLeft);
266
267 let x_hotkey_pressed = hotkeys
268 .toggle_x
269 .is_some_and(|key| keyboard_input.just_pressed(key));
270
271 let y_hotkey_pressed = hotkeys
272 .toggle_y
273 .is_some_and(|key| keyboard_input.just_pressed(key));
274
275 let z_hotkey_pressed = hotkeys
276 .toggle_z
277 .is_some_and(|key| keyboard_input.just_pressed(key));
278
279 let mut new_axes = EnumSet::empty();
280
281 if x_hotkey_pressed {
282 new_axes = if invert_modifier {
283 enum_set!(GizmoDirection::Y | GizmoDirection::Z)
284 } else {
285 enum_set!(GizmoDirection::X)
286 };
287 };
288
289 if y_hotkey_pressed {
290 new_axes = if !invert_modifier {
291 enum_set!(GizmoDirection::Y)
292 } else {
293 enum_set!(GizmoDirection::X | GizmoDirection::Z)
294 };
295 };
296
297 if z_hotkey_pressed {
298 new_axes = if !invert_modifier {
299 enum_set!(GizmoDirection::Z)
300 } else {
301 enum_set!(GizmoDirection::X | GizmoDirection::Y)
302 };
303 };
304
305 if !new_axes.is_empty() {
307 if *axes == new_axes {
308 axes.clear();
309 } else {
310 *axes = new_axes;
311 }
312 }
313
314 if gizmo_options.mode_override.is_none() {
317 axes.clear();
318 }
319
320 let rotate_hotkey_pressed = hotkeys
321 .toggle_rotate
322 .is_some_and(|key| keyboard_input.just_pressed(key));
323 let translate_hotkey_pressed = hotkeys
324 .toggle_translate
325 .is_some_and(|key| keyboard_input.just_pressed(key));
326 let scale_hotkey_pressed = hotkeys
327 .toggle_scale
328 .is_some_and(|key| keyboard_input.just_pressed(key));
329
330 let mode_kind = if rotate_hotkey_pressed {
333 if gizmo_options
335 .mode_override
336 .filter(GizmoMode::is_rotate)
337 .is_some()
338 {
339 Some(GizmoModeKind::Arcball)
340 } else {
341 Some(GizmoModeKind::Rotate)
342 }
343 } else if translate_hotkey_pressed {
344 Some(GizmoModeKind::Translate)
345 } else if scale_hotkey_pressed {
346 Some(GizmoModeKind::Scale)
347 } else {
348 gizmo_options.mode_override.map(|mode| mode.kind())
349 };
350
351 if let Some(kind) = mode_kind {
352 gizmo_options.mode_override = GizmoMode::from_kind_and_axes(kind, *axes)
353 .filter(|mode| gizmo_options.gizmo_modes.contains(*mode))
354 .or_else(|| {
355 GizmoMode::all_from_kind(kind)
356 .iter()
357 .find(|mode| gizmo_options.gizmo_modes.contains(*mode))
358 });
359 } else {
360 gizmo_options.mode_override = None;
361 }
362
363 if (hotkeys.mouse_click_deactivates
365 && mouse_input.any_just_pressed([MouseButton::Left, MouseButton::Right]))
366 || hotkeys
367 .deactivate_gizmo
368 .is_some_and(|key| keyboard_input.just_pressed(key))
369 {
370 gizmo_options.mode_override = None;
371 }
372}
373
374#[derive(Debug, Event, Default)]
375pub struct GizmoDragStarted;
376#[derive(Debug, Event, Default)]
377pub struct GizmoDragging;
378
379#[allow(clippy::too_many_arguments)]
380fn update_gizmos(
381 q_window: Query<&Window, With<PrimaryWindow>>,
382 q_gizmo_camera: Query<(&Camera, &GlobalTransform), With<GizmoCamera>>,
383 mut q_targets: Query<(Entity, &mut Transform, &mut GizmoTarget), Without<GizmoCamera>>,
384 mut drag_started: EventReader<GizmoDragStarted>,
385 mut dragging: EventReader<GizmoDragging>,
386 gizmo_options: Res<GizmoOptions>,
387 mut gizmo_storage: ResMut<GizmoStorage>,
388 mut last_cursor_pos: Local<Vec2>,
389 mut last_scaled_cursor_pos: Local<Vec2>,
390 #[cfg(feature = "gizmo_picking_backend")] hover_map: Res<HoverMap>,
391) {
392 let Ok(window) = q_window.single() else {
393 return;
395 };
396
397 let mut cursor_pos = window.cursor_position().unwrap_or_else(|| *last_cursor_pos);
398 *last_cursor_pos = cursor_pos;
399
400 let scale_factor = window.scale_factor();
401
402 let (camera, camera_transform) = {
403 let mut active_camera = None;
404
405 for camera in q_gizmo_camera.iter() {
406 if !camera.0.is_active {
407 continue;
408 }
409 if active_camera.is_some() {
410 bevy_log::warn!("Only one camera with a GizmoCamera component is supported.");
412 return;
413 }
414 active_camera = Some(camera);
415 }
416
417 match active_camera {
418 Some(camera) => camera,
419 None => return, }
421 };
422
423 let Some(viewport) = camera.logical_viewport_rect() else {
424 return;
425 };
426
427 if let Some(custom_viewport) = gizmo_options.viewport_rect {
429 let vp_ratio = viewport.size() / custom_viewport.size();
430 let mut scaled_cursor_pos = (cursor_pos - (custom_viewport.min - viewport.min)) * vp_ratio;
431 if !viewport.contains(scaled_cursor_pos) {
432 scaled_cursor_pos = *last_scaled_cursor_pos;
433 }
434 *last_scaled_cursor_pos = scaled_cursor_pos;
435 cursor_pos = scaled_cursor_pos;
436 };
437
438 let viewport = Rect::from_min_max(
439 Pos2::new(viewport.min.x, viewport.min.y),
440 Pos2::new(viewport.max.x, viewport.max.y),
441 );
442
443 let projection_matrix = camera.clip_from_view();
444
445 let view_matrix = camera_transform.compute_matrix().inverse();
446
447 let mut snap_angle = gizmo_options.snap_angle;
448 let mut snap_distance = gizmo_options.snap_distance;
449 let mut snap_scale = gizmo_options.snap_scale;
450
451 if gizmo_options.accurate_mode {
452 snap_angle /= 2.0;
453 snap_distance /= 2.0;
454 snap_scale /= 2.0;
455 }
456
457 let gizmo_config = GizmoConfig {
458 view_matrix: view_matrix.as_dmat4().into(),
459 projection_matrix: projection_matrix.as_dmat4().into(),
460 viewport,
461 modes: gizmo_options.gizmo_modes,
462 mode_override: gizmo_options.mode_override,
463 orientation: gizmo_options.gizmo_orientation,
464 pivot_point: gizmo_options.pivot_point,
465 visuals: gizmo_options.visuals,
466 snapping: gizmo_options.snapping,
467 snap_angle,
468 snap_distance,
469 snap_scale,
470 pixels_per_point: scale_factor,
471 };
472
473 #[cfg(feature = "gizmo_picking_backend")]
474 let any_gizmo_hovered = q_targets
477 .iter()
478 .any(|(entity, ..)| hover_map.iter().any(|(_, map)| map.contains_key(&entity)));
479 #[cfg(not(feature = "gizmo_picking_backend"))]
480 let any_gizmo_hovered = true;
481
482 let hovered = any_gizmo_hovered || gizmo_options.mode_override.is_some();
483
484 let gizmo_interaction = GizmoInteraction {
485 cursor_pos: (cursor_pos.x, cursor_pos.y),
486 hovered,
487 drag_started: drag_started.read().len() > 0,
488 dragging: dragging.read().len() > 0,
489 };
490
491 let mut target_entities: Vec<Entity> = vec![];
492 let mut target_transforms: Vec<Transform> = vec![];
493
494 for (entity, mut target_transform, mut gizmo_target) in &mut q_targets {
495 target_entities.push(entity);
496 target_transforms.push(*target_transform);
497
498 if gizmo_options.group_targets {
499 gizmo_storage
500 .entity_gizmo_map
501 .insert(entity, GIZMO_GROUP_UUID);
502 continue;
503 }
504
505 let mut gizmo_uuid = *gizmo_storage
506 .entity_gizmo_map
507 .entry(entity)
508 .or_insert_with(Uuid::new_v4);
509
510 if gizmo_uuid == GIZMO_GROUP_UUID {
512 gizmo_uuid = Uuid::new_v4();
513 gizmo_storage.entity_gizmo_map.insert(entity, gizmo_uuid);
514 }
515
516 let gizmo = gizmo_storage.gizmos.entry(gizmo_uuid).or_default();
517 gizmo.update_config(gizmo_config);
518
519 let gizmo_result = gizmo.update(
520 gizmo_interaction,
521 &[math::Transform {
522 translation: target_transform.translation.as_dvec3().into(),
523 rotation: target_transform.rotation.as_dquat().into(),
524 scale: target_transform.scale.as_dvec3().into(),
525 }],
526 );
527
528 let is_focused = gizmo.is_focused();
529
530 gizmo_target.is_active = gizmo_result.is_some();
531 gizmo_target.is_focused = is_focused;
532
533 if let Some((_, updated_targets)) = &gizmo_result {
534 let Some(result_transform) = updated_targets.first() else {
535 bevy_log::warn!("No transform found in GizmoResult!");
536 continue;
537 };
538
539 target_transform.translation = DVec3::from(result_transform.translation).as_vec3();
540 target_transform.rotation = DQuat::from(result_transform.rotation).as_quat();
541 target_transform.scale = DVec3::from(result_transform.scale).as_vec3();
542 }
543
544 gizmo_target.latest_result = gizmo_result.map(|(result, _)| result);
545 }
546
547 if gizmo_options.group_targets {
548 let gizmo = gizmo_storage.gizmos.entry(GIZMO_GROUP_UUID).or_default();
549 gizmo.update_config(gizmo_config);
550
551 let gizmo_result = gizmo.update(
552 gizmo_interaction,
553 target_transforms
554 .iter()
555 .map(|transform| transform_gizmo::math::Transform {
556 translation: transform.translation.as_dvec3().into(),
557 rotation: transform.rotation.as_dquat().into(),
558 scale: transform.scale.as_dvec3().into(),
559 })
560 .collect::<Vec<_>>()
561 .as_slice(),
562 );
563
564 let is_focused = gizmo.is_focused();
565
566 for (i, (_, mut target_transform, mut gizmo_target)) in q_targets.iter_mut().enumerate() {
567 gizmo_target.is_active = gizmo_result.is_some();
568 gizmo_target.is_focused = is_focused;
569
570 if let Some((_, updated_targets)) = &gizmo_result {
571 let Some(result_transform) = updated_targets.get(i) else {
572 bevy_log::warn!("No transform {i} found in GizmoResult!");
573 continue;
574 };
575
576 target_transform.translation = DVec3::from(result_transform.translation).as_vec3();
577 target_transform.rotation = DQuat::from(result_transform.rotation).as_quat();
578 target_transform.scale = DVec3::from(result_transform.scale).as_vec3();
579 }
580
581 gizmo_target.latest_result = gizmo_result.as_ref().map(|(result, _)| *result);
582 }
583 }
584
585 gizmo_storage.target_entities = target_entities;
586}
587
588fn draw_gizmos(
589 gizmo_storage: Res<GizmoStorage>,
590 mut draw_data_assets: ResMut<Assets<render::GizmoDrawData>>,
591 mut draw_data_handles: ResMut<DrawDataHandles>,
592) {
593 for (gizmo_uuid, gizmo) in &gizmo_storage.gizmos {
594 let draw_data = gizmo.draw();
595
596 let mut bevy_draw_data = render::GizmoDrawData::default();
597
598 let (asset, is_new_asset) = if let Some(handle) = draw_data_handles.handles.get(gizmo_uuid)
599 {
600 (draw_data_assets.get_mut(handle).unwrap(), false)
601 } else {
602 (&mut bevy_draw_data, true)
603 };
604
605 let viewport = &gizmo.config().viewport;
606
607 asset.0.vertices.clear();
608 asset
609 .0
610 .vertices
611 .extend(draw_data.vertices.into_iter().map(|vert| {
612 [
613 ((vert[0] - viewport.left()) / viewport.width()) * 2.0 - 1.0,
614 ((vert[1] - viewport.top()) / viewport.height()) * 2.0 - 1.0,
615 ]
616 }));
617
618 asset.0.colors = draw_data.colors;
619 asset.0.indices = draw_data.indices;
620
621 if is_new_asset {
622 let asset = draw_data_assets.add(bevy_draw_data);
623
624 draw_data_handles
625 .handles
626 .insert(*gizmo_uuid, asset.clone().into());
627 }
628 }
629}
630
631fn cleanup_old_data(
632 gizmo_options: Res<GizmoOptions>,
633 mut gizmo_storage: ResMut<GizmoStorage>,
634 mut draw_data_handles: ResMut<DrawDataHandles>,
635) {
636 let target_entities = std::mem::take(&mut gizmo_storage.target_entities);
637
638 let mut gizmos_to_keep = vec![];
639
640 if gizmo_options.group_targets && !target_entities.is_empty() {
641 gizmos_to_keep.push(GIZMO_GROUP_UUID);
642 }
643
644 gizmo_storage.entity_gizmo_map.retain(|entity, uuid| {
645 if !target_entities.contains(entity) {
646 false
647 } else {
648 gizmos_to_keep.push(*uuid);
649
650 true
651 }
652 });
653
654 gizmo_storage
655 .gizmos
656 .retain(|uuid, _| gizmos_to_keep.contains(uuid));
657
658 draw_data_handles
659 .handles
660 .retain(|uuid, _| gizmos_to_keep.contains(uuid));
661}