1use crate::geometry::intersect::ray_plane_intersection;
4use crate::renderer::{GlyphItem, GlyphType, PolylineItem};
5use crate::scene::aabb::Aabb;
6use parry3d::math::{Pose, Vector};
7use parry3d::query::{Ray, RayCast};
8
9use super::{WidgetContext, WidgetResult, any_perpendicular_pair, ctx_ray, handle_world_radius};
10
11#[derive(Clone, Copy, PartialEq, Eq, Debug)]
13enum BoxHandle {
14 Center,
16 Face(usize),
18 RotArc(usize),
20}
21
22pub struct BoxWidget {
42 pub center: glam::Vec3,
44 pub half_extents: glam::Vec3,
46 pub rotation: glam::Quat,
48 pub colour: [f32; 4],
50 pub handle_colour: [f32; 4],
52
53 hovered_handle: Option<BoxHandle>,
54 active_handle: Option<BoxHandle>,
55 drag_plane_normal: glam::Vec3,
56 drag_plane_d: f32,
57 drag_anchor_world: glam::Vec3,
58}
59
60impl BoxWidget {
61 pub fn new(center: glam::Vec3, half_extents: glam::Vec3) -> Self {
63 Self {
64 center,
65 half_extents: half_extents.max(glam::Vec3::splat(0.01)),
66 rotation: glam::Quat::IDENTITY,
67 colour: [0.3, 0.8, 0.4, 1.0],
68 handle_colour: [0.0; 4],
69 hovered_handle: None,
70 active_handle: None,
71 drag_plane_normal: glam::Vec3::Z,
72 drag_plane_d: 0.0,
73 drag_anchor_world: glam::Vec3::ZERO,
74 }
75 }
76
77 pub fn is_active(&self) -> bool {
79 self.active_handle.is_some()
80 }
81
82 pub fn obb(&self) -> (glam::Vec3, glam::Vec3, glam::Quat) {
84 (self.center, self.half_extents, self.rotation)
85 }
86
87 pub fn aabb(&self) -> Aabb {
92 let mut min = glam::Vec3::splat(f32::MAX);
93 let mut max = glam::Vec3::splat(f32::MIN);
94 let h = self.half_extents;
95 for sx in [-1.0_f32, 1.0] {
96 for sy in [-1.0_f32, 1.0] {
97 for sz in [-1.0_f32, 1.0] {
98 let p =
99 self.center + self.rotation * glam::Vec3::new(sx * h.x, sy * h.y, sz * h.z);
100 min = min.min(p);
101 max = max.max(p);
102 }
103 }
104 }
105 Aabb { min, max }
106 }
107
108 pub fn contains_point(&self, point: glam::Vec3) -> bool {
110 let local = self.rotation.inverse() * (point - self.center);
111 local.x.abs() <= self.half_extents.x
112 && local.y.abs() <= self.half_extents.y
113 && local.z.abs() <= self.half_extents.z
114 }
115
116 pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
118 let (ro, rd) = ctx_ray(ctx);
119 let mut updated = false;
120
121 if self.active_handle.is_none() {
122 let hit = self.hit_test(ro, rd, ctx);
123 if hit.is_some() || !ctx.drag_started {
127 self.hovered_handle = hit;
128 }
129 }
130
131 if ctx.drag_started {
132 if let Some(handle) = self.hovered_handle {
133 let anchor = self.handle_pos(handle);
134 let n = -glam::Vec3::from(ctx.camera.forward);
138 self.drag_plane_normal = n;
139 self.drag_plane_d = -n.dot(anchor);
140 self.drag_anchor_world = anchor;
141 self.active_handle = Some(handle);
142 }
143 }
144
145 if let Some(handle) = self.active_handle {
146 if ctx.released || (!ctx.dragging && !ctx.drag_started) {
147 self.active_handle = None;
148 self.hovered_handle = None;
149 } else {
150 match handle {
151 BoxHandle::Center => {
152 if let Some(hit) = ray_plane_intersection(
153 ro,
154 rd,
155 self.drag_plane_normal,
156 self.drag_plane_d,
157 ) {
158 let delta = hit - self.drag_anchor_world;
159 if delta.length_squared() > 1e-10 {
160 self.center += delta;
161 self.drag_anchor_world = hit;
162 updated = true;
163 }
164 }
165 }
166 BoxHandle::Face(i) => {
167 if let Some(hit) = ray_plane_intersection(
168 ro,
169 rd,
170 self.drag_plane_normal,
171 self.drag_plane_d,
172 ) {
173 let world_normal = self.rotation * Self::local_face_normal(i);
175 let proj = (hit - self.drag_anchor_world).dot(world_normal);
176 if proj.abs() > 1e-5 {
177 let axis = i / 2; let new_he = (self.half_extents[axis] + proj).max(0.01);
179 let he_delta = new_he - self.half_extents[axis];
180 self.half_extents[axis] = new_he;
181 self.center += world_normal * (he_delta * 0.5);
183 self.drag_anchor_world = hit;
184 updated = true;
185 }
186 }
187 }
188 BoxHandle::RotArc(axis_idx) => {
189 let axis = Self::world_rotation_axis(axis_idx);
193 let plane_d = -axis.dot(self.center);
194 if let Some(plane_hit) = ray_plane_intersection(ro, rd, axis, plane_d) {
195 let start_dir =
196 (self.drag_anchor_world - self.center).normalize_or_zero();
197 let new_dir = (plane_hit - self.center).normalize_or_zero();
198 if start_dir.length_squared() > 0.5 && new_dir.length_squared() > 0.5 {
199 let cos_a = start_dir.dot(new_dir).clamp(-1.0, 1.0);
200 let cross = start_dir.cross(new_dir);
201 let sign = cross.dot(axis).signum();
202 let angle = cos_a.acos() * sign;
203 if angle.abs() > 1e-5 {
204 let delta_rot = glam::Quat::from_axis_angle(axis, angle);
205 self.rotation = (delta_rot * self.rotation).normalize();
206 self.drag_anchor_world =
208 self.center + new_dir * self.arc_radius();
209 updated = true;
210 }
211 }
212 }
213 }
214 }
215 }
216 }
217
218 if updated {
219 WidgetResult::Updated
220 } else {
221 WidgetResult::None
222 }
223 }
224
225 pub fn wireframe_item(&self, id: u64) -> PolylineItem {
229 let c = self.center;
230 let h = self.half_extents;
231 let r = self.rotation;
232
233 let p =
234 |x: f32, y: f32, z: f32| -> [f32; 3] { (c + r * glam::Vec3::new(x, y, z)).to_array() };
235
236 PolylineItem {
237 positions: vec![
238 p(-h.x, -h.y, -h.z),
240 p(h.x, -h.y, -h.z),
241 p(h.x, h.y, -h.z),
242 p(-h.x, h.y, -h.z),
243 p(-h.x, -h.y, -h.z),
244 p(-h.x, -h.y, h.z),
246 p(h.x, -h.y, h.z),
247 p(h.x, h.y, h.z),
248 p(-h.x, h.y, h.z),
249 p(-h.x, -h.y, h.z),
250 p(-h.x, -h.y, -h.z),
252 p(-h.x, -h.y, h.z),
253 p(h.x, -h.y, -h.z),
254 p(h.x, -h.y, h.z),
255 p(h.x, h.y, -h.z),
256 p(h.x, h.y, h.z),
257 p(-h.x, h.y, -h.z),
258 p(-h.x, h.y, h.z),
259 ],
260 strip_lengths: vec![5, 5, 2, 2, 2, 2],
261 default_colour: self.colour,
262
263 settings: crate::scene::material::ItemSettings {
264 pick_id: crate::renderer::PickId(id),
265 ..Default::default()
266 },
267 ..PolylineItem::default()
268 }
269 }
270
271 pub fn rotation_arcs_item(&self, id: u64) -> PolylineItem {
276 const STEPS: usize = 48;
277 let c = self.center;
278 let r = self.arc_radius();
279 let arc_colours = [
280 [0.9_f32, 0.2, 0.2, 0.7], [0.2, 0.9, 0.2, 0.7], [0.2, 0.4, 1.0, 0.7], ];
284
285 let mut positions: Vec<[f32; 3]> = Vec::with_capacity((STEPS + 1) * 3);
288 let mut strip_lengths: Vec<u32> = Vec::new();
289 let _ = arc_colours; for axis_idx in 0..3_usize {
292 let axis = Self::world_rotation_axis(axis_idx);
293 let (u, v) = any_perpendicular_pair(axis);
294 for i in 0..=STEPS {
295 let a = i as f32 * std::f32::consts::TAU / STEPS as f32;
296 let (s, co) = a.sin_cos();
297 positions.push((c + u * (co * r) + v * (s * r)).to_array());
298 }
299 strip_lengths.push((STEPS + 1) as u32);
300 }
301
302 PolylineItem {
303 positions,
304 strip_lengths,
305 default_colour: self.colour,
306 line_width: 1.2,
307
308 settings: crate::scene::material::ItemSettings {
309 pick_id: crate::renderer::PickId(id),
310 ..Default::default()
311 },
312 ..PolylineItem::default()
313 }
314 }
315
316 pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
321 let all_handles = [
322 BoxHandle::Center,
323 BoxHandle::Face(0),
324 BoxHandle::Face(1),
325 BoxHandle::Face(2),
326 BoxHandle::Face(3),
327 BoxHandle::Face(4),
328 BoxHandle::Face(5),
329 BoxHandle::RotArc(0),
330 BoxHandle::RotArc(1),
331 BoxHandle::RotArc(2),
332 ];
333
334 let mut positions = Vec::with_capacity(10);
335 let mut vectors = Vec::with_capacity(10);
336 let mut scalars = Vec::with_capacity(10);
337
338 for handle in all_handles {
339 let pos = self.handle_pos(handle);
340 let target_px = if matches!(handle, BoxHandle::RotArc(_)) {
341 7.0
342 } else {
343 9.0
344 };
345 let r = handle_world_radius(pos, &ctx.camera, ctx.viewport_size.y, target_px);
346 let s = if self.hovered_handle == Some(handle) || self.active_handle == Some(handle) {
347 1.0_f32
348 } else {
349 0.2
350 };
351 positions.push(pos.to_array());
352 vectors.push([r, 0.0, 0.0]);
353 scalars.push(s);
354 }
355
356 GlyphItem {
357 positions,
358 vectors,
359 scale: 1.0,
360 scale_by_magnitude: true,
361 scalars,
362 scalar_range: Some((0.0, 1.0)),
363 glyph_type: GlyphType::Sphere,
364
365 settings: crate::scene::material::ItemSettings {
366 pick_id: crate::renderer::PickId(id_base),
367 ..Default::default()
368 },
369 default_colour: self.handle_colour,
370 use_default_colour: self.handle_colour[3] > 0.0,
371 ..GlyphItem::default()
372 }
373 }
374
375 fn handle_pos(&self, handle: BoxHandle) -> glam::Vec3 {
381 match handle {
382 BoxHandle::Center => self.center,
383 BoxHandle::Face(i) => {
384 self.center
385 + self.rotation * (Self::local_face_normal(i) * self.half_extents[i / 2])
386 }
387 BoxHandle::RotArc(i) => self.center + self.arc_grip_offset(i),
388 }
389 }
390
391 fn local_face_normal(i: usize) -> glam::Vec3 {
393 match i {
394 0 => glam::Vec3::X,
395 1 => glam::Vec3::NEG_X,
396 2 => glam::Vec3::Y,
397 3 => glam::Vec3::NEG_Y,
398 4 => glam::Vec3::Z,
399 _ => glam::Vec3::NEG_Z,
400 }
401 }
402
403 fn world_rotation_axis(i: usize) -> glam::Vec3 {
405 match i {
406 0 => glam::Vec3::X,
407 1 => glam::Vec3::Y,
408 _ => glam::Vec3::Z,
409 }
410 }
411
412 fn arc_radius(&self) -> f32 {
414 self.half_extents.length() * 1.4 + 0.1
415 }
416
417 fn arc_grip_offset(&self, i: usize) -> glam::Vec3 {
419 let r = self.arc_radius();
420 match i {
423 0 => glam::Vec3::new(0.0, r, 0.0), 1 => glam::Vec3::new(0.0, 0.0, r), _ => glam::Vec3::new(r, 0.0, 0.0), }
427 }
428
429 fn hit_test(
430 &self,
431 ray_origin: glam::Vec3,
432 ray_dir: glam::Vec3,
433 ctx: &WidgetContext,
434 ) -> Option<BoxHandle> {
435 let ray = Ray::new(
436 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
437 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
438 );
439
440 let all_handles = [
441 BoxHandle::Center,
442 BoxHandle::Face(0),
443 BoxHandle::Face(1),
444 BoxHandle::Face(2),
445 BoxHandle::Face(3),
446 BoxHandle::Face(4),
447 BoxHandle::Face(5),
448 BoxHandle::RotArc(0),
449 BoxHandle::RotArc(1),
450 BoxHandle::RotArc(2),
451 ];
452
453 let mut best: Option<(f32, BoxHandle)> = None;
454
455 for handle in all_handles {
456 let pos = self.handle_pos(handle);
457 let target_px = if matches!(handle, BoxHandle::RotArc(_)) {
458 7.0
459 } else {
460 9.0
461 };
462 let r = handle_world_radius(pos, &ctx.camera, ctx.viewport_size.y, target_px);
463 let ball = parry3d::shape::Ball::new(r);
464 let pose = Pose::from_parts([pos.x, pos.y, pos.z].into(), glam::Quat::IDENTITY);
465 if let Some(t) = ball.cast_ray(&pose, &ray, f32::MAX, true) {
466 if best.is_none() || t < best.unwrap().0 {
467 best = Some((t, handle));
468 }
469 }
470 }
471
472 best.map(|(_, h)| h)
473 }
474}