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