Skip to main content

viewport_lib/interaction/widgets/
mod.rs

1//! Interactive 3D probe and region widgets.
2//!
3//! Each widget is a pure CPU state struct (like `Gizmo`) that the host app owns.
4//! Push render items from the widget into `SceneFrame` each frame, call `update()`
5//! to advance state, and read public fields for results.
6//!
7//! Suppress orbit while a widget is active using the same pattern as
8//! `ManipulationController`:
9//!
10//! ```rust,ignore
11//! if probe.is_active() {
12//!     orbit.resolve();
13//! } else {
14//!     orbit.apply_to_camera(&mut camera);
15//! }
16//! ```
17
18pub mod box_widget;
19pub mod line_probe;
20pub mod sphere;
21
22pub use box_widget::BoxWidget;
23pub use line_probe::LineProbeWidget;
24pub use sphere::SphereWidget;
25
26use crate::renderer::RenderCamera;
27
28// ---------------------------------------------------------------------------
29// WidgetContext
30// ---------------------------------------------------------------------------
31
32/// Per-frame input state passed to widget `update()` methods.
33///
34/// Build this from the `ActionFrame` and `CameraFrame` your app already has.
35/// Mirrors the shape of [`crate::ManipulationContext`].
36#[derive(Clone, Debug)]
37pub struct WidgetContext {
38    /// Camera state for this frame (used for ray construction and drag projection).
39    pub camera: RenderCamera,
40    /// Viewport width and height in pixels.
41    pub viewport_size: glam::Vec2,
42    /// Mouse cursor position relative to the viewport top-left, in pixels.
43    pub cursor_viewport: glam::Vec2,
44    /// True on the first frame that a left-button drag crosses the egui drag threshold.
45    pub drag_started: bool,
46    /// True while the left mouse button is held after crossing the drag threshold.
47    pub dragging: bool,
48    /// True on the frame the left mouse button is released.
49    pub released: bool,
50}
51
52// ---------------------------------------------------------------------------
53// WidgetResult
54// ---------------------------------------------------------------------------
55
56/// Result returned by widget `update()` calls.
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum WidgetResult {
59    /// Nothing changed this frame.
60    None,
61    /// The widget state changed (endpoint moved, size changed, etc.).
62    Updated,
63}
64
65// ---------------------------------------------------------------------------
66// Shared internal helpers
67// ---------------------------------------------------------------------------
68
69/// Compute a world-space radius that maps to `target_px` pixels on screen.
70///
71/// Used to keep handle spheres at a constant apparent screen size.
72pub(super) fn handle_world_radius(
73    pos: glam::Vec3,
74    camera: &RenderCamera,
75    viewport_height: f32,
76    target_px: f32,
77) -> f32 {
78    let eye = glam::Vec3::from(camera.eye_position);
79    let dist = (pos - eye).length().max(0.001);
80    let world_per_px = 2.0 * (camera.fov * 0.5).tan() * dist / viewport_height.max(1.0);
81    world_per_px * target_px
82}
83
84/// Build a ray from the context cursor position.
85pub(super) fn ctx_ray(ctx: &WidgetContext) -> (glam::Vec3, glam::Vec3) {
86    let vp = ctx.camera.projection * ctx.camera.view;
87    crate::interaction::picking::screen_to_ray(ctx.cursor_viewport, ctx.viewport_size, vp.inverse())
88}
89
90/// Shortest distance from a ray to a point.
91pub(super) fn ray_point_dist(
92    ray_origin: glam::Vec3,
93    ray_dir: glam::Vec3,
94    point: glam::Vec3,
95) -> f32 {
96    let t = (point - ray_origin).dot(ray_dir).max(0.0);
97    (ray_origin + ray_dir * t - point).length()
98}