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
18/// Axes orientation indicator drawn in the viewport corner.
19pub mod axes_indicator;
20pub mod box_widget;
21pub mod cylinder;
22pub mod disk;
23pub mod line_probe;
24pub mod plane;
25pub mod polyline_widget;
26pub mod sphere;
27pub mod spline;
28
29pub use box_widget::BoxWidget;
30pub use cylinder::CylinderWidget;
31pub use disk::DiskWidget;
32pub use line_probe::LineProbeWidget;
33pub use plane::PlaneWidget;
34pub use polyline_widget::PolylineWidget;
35pub use sphere::SphereWidget;
36pub use spline::SplineWidget;
37
38use crate::renderer::RenderCamera;
39
40// ---------------------------------------------------------------------------
41// WidgetContext
42// ---------------------------------------------------------------------------
43
44/// Per-frame input state passed to widget `update()` methods.
45///
46/// Build this from the `ActionFrame` and `CameraFrame` your app already has.
47/// Mirrors the shape of [`crate::ManipulationContext`].
48#[derive(Clone, Debug)]
49pub struct WidgetContext {
50    /// Camera state for this frame (used for ray construction and drag projection).
51    pub camera: RenderCamera,
52    /// Viewport width and height in pixels.
53    pub viewport_size: glam::Vec2,
54    /// Mouse cursor position relative to the viewport top-left, in pixels.
55    pub cursor_viewport: glam::Vec2,
56    /// True on the first frame that a left-button drag crosses the egui drag threshold.
57    pub drag_started: bool,
58    /// True while the left mouse button is held after crossing the drag threshold.
59    pub dragging: bool,
60    /// True on the frame the left mouse button is released.
61    pub released: bool,
62    /// True on the second click within the double-click time window.
63    ///
64    /// Used by `PolylineWidget` to insert or remove control points. Set from the
65    /// framework's double-click event (e.g. `egui::Response::double_clicked()`).
66    /// Leave `false` if the host does not need double-click interactions.
67    pub double_clicked: bool,
68}
69
70// ---------------------------------------------------------------------------
71// WidgetResult
72// ---------------------------------------------------------------------------
73
74/// Result returned by widget `update()` calls.
75#[non_exhaustive]
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum WidgetResult {
78    /// Nothing changed this frame.
79    None,
80    /// The widget state changed (endpoint moved, size changed, point added/removed, etc.).
81    Updated,
82}
83
84// ---------------------------------------------------------------------------
85// Shared internal helpers
86// ---------------------------------------------------------------------------
87
88/// Compute a world-space radius that maps to `target_px` pixels on screen.
89///
90/// Used to keep handle spheres at a constant apparent screen size.
91pub(super) fn handle_world_radius(
92    pos: glam::Vec3,
93    camera: &RenderCamera,
94    viewport_height: f32,
95    target_px: f32,
96) -> f32 {
97    let eye = glam::Vec3::from(camera.eye_position);
98    let dist = (pos - eye).length().max(0.001);
99    let world_per_px = 2.0 * (camera.fov * 0.5).tan() * dist / viewport_height.max(1.0);
100    world_per_px * target_px
101}
102
103/// Build a ray from the context cursor position.
104pub(super) fn ctx_ray(ctx: &WidgetContext) -> (glam::Vec3, glam::Vec3) {
105    let vp = ctx.camera.projection * ctx.camera.view;
106    crate::interaction::query::picking::screen_to_ray(
107        ctx.cursor_viewport,
108        ctx.viewport_size,
109        vp.inverse(),
110    )
111}
112
113/// Shortest distance from a ray to a point.
114pub(super) fn ray_point_dist(
115    ray_origin: glam::Vec3,
116    ray_dir: glam::Vec3,
117    point: glam::Vec3,
118) -> f32 {
119    let t = (point - ray_origin).dot(ray_dir).max(0.0);
120    (ray_origin + ray_dir * t - point).length()
121}
122
123/// Returns a unit vector perpendicular to `n`.
124pub(super) fn any_perpendicular(n: glam::Vec3) -> glam::Vec3 {
125    let len = n.length();
126    if len < 1e-6 {
127        return glam::Vec3::X;
128    }
129    let n = n / len;
130    if n.x.abs() < 0.9 {
131        n.cross(glam::Vec3::X).normalize()
132    } else {
133        n.cross(glam::Vec3::Y).normalize()
134    }
135}
136
137/// Returns two unit vectors `(u, v)` that are mutually perpendicular and perpendicular to `n`.
138pub(super) fn any_perpendicular_pair(n: glam::Vec3) -> (glam::Vec3, glam::Vec3) {
139    let u = any_perpendicular(n);
140    let len = n.length();
141    let n_unit = if len > 1e-6 { n / len } else { glam::Vec3::Z };
142    let v = n_unit.cross(u);
143    (u, v)
144}