viewport_lib/interaction/widgets/
plane.rs1use crate::interaction::clip_plane::ray_plane_intersection;
4use crate::renderer::{GlyphItem, GlyphType, PolylineItem};
5use parry3d::math::{Pose, Vector};
6use parry3d::query::{Ray, RayCast};
7
8use super::{WidgetContext, WidgetResult, any_perpendicular_pair, ctx_ray, handle_world_radius, ray_point_dist};
9
10#[derive(Clone, Copy, PartialEq, Eq, Debug)]
11enum PlaneHandle {
12 Center,
13 NormalTip,
14}
15
16pub struct PlaneWidget {
35 pub center: glam::Vec3,
37 pub normal: glam::Vec3,
39 pub color: [f32; 4],
41 pub handle_color: [f32; 4],
43 pub display_half_size: f32,
45 pub normal_display_length: f32,
47
48 hovered_handle: Option<PlaneHandle>,
49 active_handle: Option<PlaneHandle>,
50 drag_plane_normal: glam::Vec3,
51 drag_plane_d: f32,
52 drag_anchor: glam::Vec3,
53}
54
55impl PlaneWidget {
56 pub fn new(center: glam::Vec3, normal: glam::Vec3) -> Self {
60 let len = normal.length();
61 let normal = if len > 1e-6 { normal / len } else { glam::Vec3::Z };
62 Self {
63 center,
64 normal,
65 color: [0.3, 0.7, 1.0, 1.0],
66 handle_color: [0.0; 4],
67 display_half_size: 1.5,
68 normal_display_length: 2.0,
69 hovered_handle: None,
70 active_handle: None,
71 drag_plane_normal: glam::Vec3::Z,
72 drag_plane_d: 0.0,
73 drag_anchor: glam::Vec3::ZERO,
74 }
75 }
76
77 pub fn is_active(&self) -> bool {
79 self.active_handle.is_some()
80 }
81
82 pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
84 let (ro, rd) = ctx_ray(ctx);
85 let mut updated = false;
86
87 if self.active_handle.is_none() {
88 let hit = self.hit_test(ro, rd, ctx);
89 if hit.is_some() || !ctx.drag_started {
90 self.hovered_handle = hit;
91 }
92 }
93
94 if ctx.drag_started {
95 if let Some(handle) = self.hovered_handle {
96 let anchor = match handle {
97 PlaneHandle::Center => self.center,
98 PlaneHandle::NormalTip => self.normal_tip_pos(),
99 };
100 let n = -glam::Vec3::from(ctx.camera.forward);
101 self.drag_plane_normal = n;
102 self.drag_plane_d = -n.dot(anchor);
103 self.drag_anchor = anchor;
104 self.active_handle = Some(handle);
105 }
106 }
107
108 if let Some(handle) = self.active_handle {
109 if ctx.released || (!ctx.dragging && !ctx.drag_started) {
110 self.active_handle = None;
111 self.hovered_handle = None;
112 } else if let Some(hit) =
113 ray_plane_intersection(ro, rd, self.drag_plane_normal, self.drag_plane_d)
114 {
115 match handle {
116 PlaneHandle::Center => {
117 let delta = hit - self.drag_anchor;
118 if delta.length_squared() > 1e-10 {
119 self.center += delta;
120 self.drag_anchor = hit;
121 updated = true;
122 }
123 }
124 PlaneHandle::NormalTip => {
125 let dir = hit - self.center;
126 let len = dir.length();
127 if len > 1e-3 {
128 let new_normal = dir / len;
129 if (new_normal - self.normal).length_squared() > 1e-8 {
130 self.normal = new_normal;
131 updated = true;
132 }
133 }
134 }
135 }
136 }
137 }
138
139 if updated { WidgetResult::Updated } else { WidgetResult::None }
140 }
141
142 pub fn plane_item(&self, id: u64) -> PolylineItem {
147 let (u, v) = any_perpendicular_pair(self.normal);
148 let s = self.display_half_size;
149 let c = self.center;
150 let tip = self.normal_tip_pos();
151
152 let positions = vec![
154 (c + u * s + v * s).to_array(),
155 (c - u * s + v * s).to_array(),
156 (c - u * s - v * s).to_array(),
157 (c + u * s - v * s).to_array(),
158 (c + u * s + v * s).to_array(),
159 c.to_array(),
160 tip.to_array(),
161 ];
162
163 PolylineItem {
164 positions,
165 strip_lengths: vec![5, 2],
166 default_color: self.color,
167 line_width: 1.5,
168 id,
169 ..PolylineItem::default()
170 }
171 }
172
173 pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
177 let tip = self.normal_tip_pos();
178 let rc = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
179 let rt = handle_world_radius(tip, &ctx.camera, ctx.viewport_size.y, 8.0);
180
181 let sc = if matches!(self.hovered_handle, Some(PlaneHandle::Center))
182 || matches!(self.active_handle, Some(PlaneHandle::Center))
183 {
184 1.0_f32
185 } else {
186 0.2
187 };
188 let st = if matches!(self.hovered_handle, Some(PlaneHandle::NormalTip))
189 || matches!(self.active_handle, Some(PlaneHandle::NormalTip))
190 {
191 1.0_f32
192 } else {
193 0.2
194 };
195
196 GlyphItem {
197 positions: vec![self.center.to_array(), tip.to_array()],
198 vectors: vec![[rc, 0.0, 0.0], [rt, 0.0, 0.0]],
199 scale: 1.0,
200 scale_by_magnitude: true,
201 scalars: vec![sc, st],
202 scalar_range: Some((0.0, 1.0)),
203 glyph_type: GlyphType::Sphere,
204 id: id_base,
205 default_color: self.handle_color,
206 use_default_color: self.handle_color[3] > 0.0,
207 ..GlyphItem::default()
208 }
209 }
210
211 fn normal_tip_pos(&self) -> glam::Vec3 {
216 self.center + self.normal * self.normal_display_length
217 }
218
219 fn hit_test(
220 &self,
221 ray_origin: glam::Vec3,
222 ray_dir: glam::Vec3,
223 ctx: &WidgetContext,
224 ) -> Option<PlaneHandle> {
225 let tip = self.normal_tip_pos();
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
231 let rc = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
232 let rt = handle_world_radius(tip, &ctx.camera, ctx.viewport_size.y, 8.0);
233
234 let dc = ray_point_dist(ray_origin, ray_dir, self.center);
235 let dt = ray_point_dist(ray_origin, ray_dir, tip);
236
237 let center_ball = parry3d::shape::Ball::new(rc);
238 let tip_ball = parry3d::shape::Ball::new(rt);
239 let center_pose = Pose::from_parts(
240 [self.center.x, self.center.y, self.center.z].into(),
241 glam::Quat::IDENTITY,
242 );
243 let tip_pose = Pose::from_parts([tip.x, tip.y, tip.z].into(), glam::Quat::IDENTITY);
244
245 let tc = if dc < rc { center_ball.cast_ray(¢er_pose, &ray, f32::MAX, true) } else { None };
246 let tt = if dt < rt { tip_ball.cast_ray(&tip_pose, &ray, f32::MAX, true) } else { None };
247
248 match (tc, tt) {
249 (Some(a), Some(b)) => Some(if a <= b { PlaneHandle::Center } else { PlaneHandle::NormalTip }),
250 (Some(_), None) => Some(PlaneHandle::Center),
251 (None, Some(_)) => Some(PlaneHandle::NormalTip),
252 (None, None) => None,
253 }
254 }
255}