viewport_lib/interaction/widgets/
box_widget.rs1use crate::interaction::clip_plane::ray_plane_intersection;
4use crate::renderer::{GlyphItem, GlyphType, PolylineItem, aabb_wireframe_polyline};
5use crate::scene::aabb::Aabb;
6use parry3d::math::{Pose, Vector};
7use parry3d::query::{Ray, RayCast};
8
9use super::{WidgetContext, WidgetResult, ctx_ray, handle_world_radius};
10
11#[derive(Clone, Copy, PartialEq, Eq, Debug)]
13enum BoxHandle {
14 Center,
16 Face(usize),
18}
19
20pub struct BoxWidget {
36 pub center: glam::Vec3,
38 pub half_extents: glam::Vec3,
40 pub color: [f32; 4],
42
43 hovered_handle: Option<BoxHandle>,
44 active_handle: Option<BoxHandle>,
45 drag_plane_normal: glam::Vec3,
46 drag_plane_d: f32,
47 drag_anchor_world: glam::Vec3,
48}
49
50impl BoxWidget {
51 pub fn new(center: glam::Vec3, half_extents: glam::Vec3) -> Self {
53 Self {
54 center,
55 half_extents: half_extents.max(glam::Vec3::splat(0.01)),
56 color: [0.3, 0.8, 0.4, 1.0],
57 hovered_handle: None,
58 active_handle: None,
59 drag_plane_normal: glam::Vec3::Z,
60 drag_plane_d: 0.0,
61 drag_anchor_world: glam::Vec3::ZERO,
62 }
63 }
64
65 pub fn is_active(&self) -> bool {
67 self.active_handle.is_some()
68 }
69
70 pub fn aabb(&self) -> Aabb {
72 Aabb {
73 min: self.center - self.half_extents,
74 max: self.center + self.half_extents,
75 }
76 }
77
78 pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
80 let (ro, rd) = ctx_ray(ctx);
81 let mut updated = false;
82
83 if self.active_handle.is_none() {
84 self.hovered_handle = self.hit_test(ro, rd, ctx);
85 }
86
87 if ctx.drag_started {
88 if let Some(handle) = self.hovered_handle {
89 let anchor = self.handle_pos(handle);
90 let n = -glam::Vec3::from(ctx.camera.forward);
95 self.drag_plane_normal = n;
96 self.drag_plane_d = -n.dot(anchor);
97 self.drag_anchor_world = anchor;
98 self.active_handle = Some(handle);
99 }
100 }
101
102 if let Some(handle) = self.active_handle {
103 if ctx.released || (!ctx.dragging && !ctx.drag_started) {
104 self.active_handle = None;
105 self.hovered_handle = None;
106 } else if let Some(hit) =
107 ray_plane_intersection(ro, rd, self.drag_plane_normal, self.drag_plane_d)
108 {
109 match handle {
110 BoxHandle::Center => {
111 let delta = hit - self.drag_anchor_world;
112 if delta.length_squared() > 1e-10 {
113 self.center += delta;
114 self.drag_anchor_world = hit;
115 updated = true;
116 }
117 }
118 BoxHandle::Face(i) => {
119 let normal = Self::face_normal(i);
123 let proj = (hit - self.drag_anchor_world).dot(normal);
124 if proj.abs() > 1e-5 {
125 let axis = i / 2; let new_he = (self.half_extents[axis] + proj).max(0.01);
127 let he_delta = new_he - self.half_extents[axis];
128 self.half_extents[axis] = new_he;
129 self.center += normal * (he_delta * 0.5);
131 self.drag_anchor_world = hit;
132 updated = true;
133 }
134 }
135 }
136 }
137 }
138
139 if updated { WidgetResult::Updated } else { WidgetResult::None }
140 }
141
142 pub fn wireframe_item(&self, id: u64) -> PolylineItem {
146 let mut item = aabb_wireframe_polyline(&self.aabb(), self.color);
147 item.id = id;
148 item
149 }
150
151 pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
156 let handles = [
157 BoxHandle::Center,
158 BoxHandle::Face(0),
159 BoxHandle::Face(1),
160 BoxHandle::Face(2),
161 BoxHandle::Face(3),
162 BoxHandle::Face(4),
163 BoxHandle::Face(5),
164 ];
165
166 let mut positions = Vec::with_capacity(7);
167 let mut vectors = Vec::with_capacity(7);
168 let mut scalars = Vec::with_capacity(7);
169
170 for handle in handles {
171 let pos = self.handle_pos(handle);
172 let r = handle_world_radius(pos, &ctx.camera, ctx.viewport_size.y, 9.0);
173 let s = if self.hovered_handle == Some(handle) || self.active_handle == Some(handle) {
174 1.0_f32
175 } else {
176 0.2
177 };
178 positions.push(pos.to_array());
179 vectors.push([r, 0.0, 0.0]);
180 scalars.push(s);
181 }
182
183 GlyphItem {
184 positions,
185 vectors,
186 scale: 1.0,
187 scale_by_magnitude: true,
188 scalars,
189 scalar_range: Some((0.0, 1.0)),
190 glyph_type: GlyphType::Sphere,
191 id: id_base,
192 ..GlyphItem::default()
193 }
194 }
195
196 fn handle_pos(&self, handle: BoxHandle) -> glam::Vec3 {
202 match handle {
203 BoxHandle::Center => self.center,
204 BoxHandle::Face(i) => self.center + Self::face_normal(i) * self.half_extents[i / 2],
205 }
206 }
207
208 fn face_normal(i: usize) -> glam::Vec3 {
210 match i {
211 0 => glam::Vec3::X,
212 1 => glam::Vec3::NEG_X,
213 2 => glam::Vec3::Y,
214 3 => glam::Vec3::NEG_Y,
215 4 => glam::Vec3::Z,
216 _ => glam::Vec3::NEG_Z,
217 }
218 }
219
220 fn hit_test(
221 &self,
222 ray_origin: glam::Vec3,
223 ray_dir: glam::Vec3,
224 ctx: &WidgetContext,
225 ) -> Option<BoxHandle> {
226 let ray = Ray::new(
227 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
228 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
229 );
230 let identity = glam::Quat::IDENTITY;
231
232 let handles = [
233 BoxHandle::Center,
234 BoxHandle::Face(0),
235 BoxHandle::Face(1),
236 BoxHandle::Face(2),
237 BoxHandle::Face(3),
238 BoxHandle::Face(4),
239 BoxHandle::Face(5),
240 ];
241
242 let mut best: Option<(f32, BoxHandle)> = None;
243
244 for handle in handles {
245 let pos = self.handle_pos(handle);
246 let r = handle_world_radius(pos, &ctx.camera, ctx.viewport_size.y, 12.0);
247 let ball = parry3d::shape::Ball::new(r);
248 let pose = Pose::from_parts([pos.x, pos.y, pos.z].into(), identity);
249 if let Some(t) = ball.cast_ray(&pose, &ray, f32::MAX, true) {
250 if best.is_none() || t < best.unwrap().0 {
251 best = Some((t, handle));
252 }
253 }
254 }
255
256 best.map(|(_, h)| h)
257 }
258}