Skip to main content

viewport_lib/interaction/widgets/
box_widget.rs

1//! Box widget: draggable center handle and six face handles.
2
3use 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/// Which handle on the box is being interacted with.
12#[derive(Clone, Copy, PartialEq, Eq, Debug)]
13enum BoxHandle {
14    /// Center: moves the whole box.
15    Center,
16    /// One of the six face handles. Index: 0=+X, 1=-X, 2=+Y, 3=-Y, 4=+Z, 5=-Z.
17    Face(usize),
18}
19
20/// An interactive axis-aligned box widget with a center handle and six face handles.
21///
22/// Use `wireframe_item()` for the box outline (push into `fd.scene.polylines`) and
23/// `handle_glyphs()` for the 7 draggable sphere handles (push into `fd.scene.glyphs`).
24///
25/// # Usage
26///
27/// ```rust,ignore
28/// let mut bw = BoxWidget::new(glam::Vec3::ZERO, glam::Vec3::splat(2.0));
29///
30/// // Each frame:
31/// bw.update(&ctx);
32/// fd.scene.polylines.push(bw.wireframe_item(BOX_ID));
33/// fd.scene.glyphs.push(bw.handle_glyphs(HANDLE_ID, &ctx));
34/// ```
35pub struct BoxWidget {
36    /// World-space center of the box.
37    pub center: glam::Vec3,
38    /// Half-extents along each world axis.
39    pub half_extents: glam::Vec3,
40    /// RGBA color for the wireframe outline.
41    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    /// Create a new box widget.
52    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    /// True while a drag session is in progress.
66    pub fn is_active(&self) -> bool {
67        self.active_handle.is_some()
68    }
69
70    /// The current AABB of the box (center +/- half_extents).
71    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    /// Process input for this frame. Returns `Updated` if state changed.
79    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                // Always use a camera-facing drag plane. For face handles the
91                // resize amount is derived by projecting the movement onto the
92                // face normal after the hit is computed, not by constraining the
93                // plane itself to the face axis (which would make proj always 0).
94                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                        // Move this face outward while keeping the opposite face fixed.
120                        // proj > 0 means movement in the face normal direction (outward)
121                        // for both positive and negative faces, so no sign correction needed.
122                        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; // 0=X, 1=Y, 2=Z
126                            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                            // Keep opposite face fixed: shift center by half the change.
130                            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    /// Build a `PolylineItem` for the box wireframe outline.
143    ///
144    /// `id` is the pick ID (0 = not pickable).
145    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    /// Build a `GlyphItem` with 7 sphere handles: center + 6 face centers.
152    ///
153    /// `id_base` is the pick ID for the center handle; face handles use
154    /// `id_base + 1` through `id_base + 6`.
155    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    // -----------------------------------------------------------------------
197    // Internal
198    // -----------------------------------------------------------------------
199
200    /// World position of a handle.
201    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    /// Outward unit normal for face index 0..5 (+X,-X,+Y,-Y,+Z,-Z).
209    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}