1use std::collections::HashMap;
2
3use bevy::prelude::*;
4use trackball::{Clamp, Delta, Fixed, Frame, Scope, approx::AbsDiffEq, nalgebra::Point2};
5
6#[derive(Component, Debug)]
8pub struct TrackballCamera {
9 pub frame: Frame<f32>,
17 old_frame: Frame<f32>,
18 pub scope: Scope<f32>,
27 old_scope: Scope<f32>,
28 old_max: Point2<f32>,
29 pub blend: f32,
33 pub reset: Frame<f32>,
37 pub clamp: Option<Box<dyn Clamp<f32>>>,
41 pub(crate) delta: Option<Delta<f32>>,
42 pub group: HashMap<Entity, bool>,
52}
53
54impl TrackballCamera {
55 #[must_use]
57 pub fn look_at(target: Vec3, eye: Vec3, up: Vec3) -> Self {
58 let frame = Frame::look_at(target.into(), &eye.into(), &up.into());
59 Self {
60 frame,
61 old_frame: Frame::default(),
62 scope: Scope::default(),
63 old_scope: Scope::default(),
64 old_max: Point2::default(),
65 blend: 40.0,
66 reset: frame,
67 clamp: None,
68 delta: None,
69 group: HashMap::default(),
70 }
71 }
72 #[must_use]
74 #[allow(clippy::type_complexity)]
75 pub const fn with_scope(mut self, scope: Scope<f32>) -> Self {
76 self.scope = scope;
77 self
78 }
79 #[must_use]
81 pub const fn with_blend(mut self, blend: f32) -> Self {
82 self.blend = blend;
83 self
84 }
85 #[must_use]
87 #[allow(clippy::type_complexity)]
88 pub const fn with_reset(mut self, reset: Frame<f32>) -> Self {
89 self.reset = reset;
90 self
91 }
92 #[must_use]
94 #[allow(clippy::type_complexity)]
95 pub fn with_clamp(mut self, clamp: impl Clamp<f32>) -> Self {
96 self.clamp = Some(Box::new(clamp));
97 self
98 }
99 #[must_use]
101 pub fn add_controller(mut self, id: Entity, rigid: bool) -> Self {
102 self.group.insert(id, rigid);
103 self
104 }
105}
106
107#[allow(clippy::needless_pass_by_value)]
108pub fn trackball_camera(
109 time: Res<Time>,
110 mut cameras: Query<(
111 &Camera,
112 &mut TrackballCamera,
113 &mut Transform,
114 &mut Projection,
115 )>,
116) {
117 for (camera, mut trackball, mut transform, mut projection) in &mut cameras {
118 let Some(max) = camera.logical_viewport_size().map(Point2::from) else {
119 continue;
120 };
121 #[allow(clippy::float_cmp)]
122 let new_zat = trackball.frame.distance() != trackball.old_frame.distance();
123 if trackball.frame != trackball.old_frame {
124 if trackball.old_frame == Frame::default() {
125 trackball.old_frame = trackball.frame;
126 }
127 let blend = (trackball.blend * 1e-3).clamp(0.0, 1.0);
128 let blend = 1.0 - 0.5f32.powf(time.delta_secs() / blend);
129 trackball.old_frame = trackball
130 .old_frame
131 .abs_diff_ne(&trackball.frame, f32::EPSILON.sqrt())
132 .then(|| {
133 trackball
134 .old_frame
135 .try_lerp_slerp(&trackball.frame, blend, 0.0)
136 .map(|mut frame| {
137 frame.renormalize();
138 frame
139 })
140 })
141 .flatten()
142 .unwrap_or(trackball.frame);
143 let view = trackball.old_frame.view();
144 transform.translation = view.translation.into();
145 transform.rotation = view.rotation.into();
146 }
147 let new_scope = trackball.scope != trackball.old_scope;
148 let new_max = max != trackball.old_max;
149 trackball.old_scope = trackball.scope;
150 trackball.old_max = max;
151 let fov = trackball.scope.fov();
152 let zat = trackball.old_frame.distance();
153 let (near, far) = trackball.scope.clip_planes(zat);
154 if trackball.scope.ortho() {
155 if new_scope || new_max || new_zat {
156 let (_max, upp) = fov.max_and_upp(zat, &max);
157 *projection = Projection::Orthographic(OrthographicProjection {
158 near,
159 far,
160 scale: upp,
161 ..OrthographicProjection::default_3d()
162 });
163 }
164 } else if new_scope || (new_max && !matches!(fov, Fixed::Ver(_fov))) {
165 let fov = fov.to_ver(&max).into_inner();
166 let aspect_ratio = max.x / max.y;
167 *projection = Projection::Perspective(PerspectiveProjection {
168 fov,
169 aspect_ratio,
170 near,
171 far,
172 });
173 }
174 }
175}