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::{
9 WidgetContext, WidgetResult, any_perpendicular_pair, ctx_ray, handle_world_radius,
10 ray_point_dist,
11};
12
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14enum PlaneHandle {
15 Center,
16 NormalTip,
17}
18
19pub struct PlaneWidget {
38 pub center: glam::Vec3,
40 pub normal: glam::Vec3,
42 pub colour: [f32; 4],
44 pub handle_colour: [f32; 4],
46 pub display_half_size: f32,
48 pub normal_display_length: f32,
50
51 hovered_handle: Option<PlaneHandle>,
52 active_handle: Option<PlaneHandle>,
53 drag_plane_normal: glam::Vec3,
54 drag_plane_d: f32,
55 drag_anchor: glam::Vec3,
56}
57
58impl PlaneWidget {
59 pub fn new(center: glam::Vec3, normal: glam::Vec3) -> Self {
63 let len = normal.length();
64 let normal = if len > 1e-6 {
65 normal / len
66 } else {
67 glam::Vec3::Z
68 };
69 Self {
70 center,
71 normal,
72 colour: [0.3, 0.7, 1.0, 1.0],
73 handle_colour: [0.0; 4],
74 display_half_size: 1.5,
75 normal_display_length: 2.0,
76 hovered_handle: None,
77 active_handle: None,
78 drag_plane_normal: glam::Vec3::Z,
79 drag_plane_d: 0.0,
80 drag_anchor: glam::Vec3::ZERO,
81 }
82 }
83
84 pub fn is_active(&self) -> bool {
86 self.active_handle.is_some()
87 }
88
89 pub fn update(&mut self, ctx: &WidgetContext) -> WidgetResult {
91 let (ro, rd) = ctx_ray(ctx);
92 let mut updated = false;
93
94 if self.active_handle.is_none() {
95 let hit = self.hit_test(ro, rd, ctx);
96 if hit.is_some() || !ctx.drag_started {
97 self.hovered_handle = hit;
98 }
99 }
100
101 if ctx.drag_started {
102 if let Some(handle) = self.hovered_handle {
103 let anchor = match handle {
104 PlaneHandle::Center => self.center,
105 PlaneHandle::NormalTip => self.normal_tip_pos(),
106 };
107 let n = -glam::Vec3::from(ctx.camera.forward);
108 self.drag_plane_normal = n;
109 self.drag_plane_d = -n.dot(anchor);
110 self.drag_anchor = anchor;
111 self.active_handle = Some(handle);
112 }
113 }
114
115 if let Some(handle) = self.active_handle {
116 if ctx.released || (!ctx.dragging && !ctx.drag_started) {
117 self.active_handle = None;
118 self.hovered_handle = None;
119 } else if let Some(hit) =
120 ray_plane_intersection(ro, rd, self.drag_plane_normal, self.drag_plane_d)
121 {
122 match handle {
123 PlaneHandle::Center => {
124 let delta = hit - self.drag_anchor;
125 if delta.length_squared() > 1e-10 {
126 self.center += delta;
127 self.drag_anchor = hit;
128 updated = true;
129 }
130 }
131 PlaneHandle::NormalTip => {
132 let dir = hit - self.center;
133 let len = dir.length();
134 if len > 1e-3 {
135 let new_normal = dir / len;
136 if (new_normal - self.normal).length_squared() > 1e-8 {
137 self.normal = new_normal;
138 updated = true;
139 }
140 }
141 }
142 }
143 }
144 }
145
146 if updated {
147 WidgetResult::Updated
148 } else {
149 WidgetResult::None
150 }
151 }
152
153 pub fn plane_item(&self, id: u64) -> PolylineItem {
158 let (u, v) = any_perpendicular_pair(self.normal);
159 let s = self.display_half_size;
160 let c = self.center;
161 let tip = self.normal_tip_pos();
162
163 let positions = vec![
165 (c + u * s + v * s).to_array(),
166 (c - u * s + v * s).to_array(),
167 (c - u * s - v * s).to_array(),
168 (c + u * s - v * s).to_array(),
169 (c + u * s + v * s).to_array(),
170 c.to_array(),
171 tip.to_array(),
172 ];
173
174 PolylineItem {
175 positions,
176 strip_lengths: vec![5, 2],
177 default_colour: self.colour,
178 line_width: 1.5,
179 id,
180 ..PolylineItem::default()
181 }
182 }
183
184 pub fn handle_glyphs(&self, id_base: u64, ctx: &WidgetContext) -> GlyphItem {
188 let tip = self.normal_tip_pos();
189 let rc = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
190 let rt = handle_world_radius(tip, &ctx.camera, ctx.viewport_size.y, 8.0);
191
192 let sc = if matches!(self.hovered_handle, Some(PlaneHandle::Center))
193 || matches!(self.active_handle, Some(PlaneHandle::Center))
194 {
195 1.0_f32
196 } else {
197 0.2
198 };
199 let st = if matches!(self.hovered_handle, Some(PlaneHandle::NormalTip))
200 || matches!(self.active_handle, Some(PlaneHandle::NormalTip))
201 {
202 1.0_f32
203 } else {
204 0.2
205 };
206
207 GlyphItem {
208 positions: vec![self.center.to_array(), tip.to_array()],
209 vectors: vec![[rc, 0.0, 0.0], [rt, 0.0, 0.0]],
210 scale: 1.0,
211 scale_by_magnitude: true,
212 scalars: vec![sc, st],
213 scalar_range: Some((0.0, 1.0)),
214 glyph_type: GlyphType::Sphere,
215 id: id_base,
216 default_colour: self.handle_colour,
217 use_default_colour: self.handle_colour[3] > 0.0,
218 ..GlyphItem::default()
219 }
220 }
221
222 fn normal_tip_pos(&self) -> glam::Vec3 {
227 self.center + self.normal * self.normal_display_length
228 }
229
230 fn hit_test(
231 &self,
232 ray_origin: glam::Vec3,
233 ray_dir: glam::Vec3,
234 ctx: &WidgetContext,
235 ) -> Option<PlaneHandle> {
236 let tip = self.normal_tip_pos();
237 let ray = Ray::new(
238 Vector::new(ray_origin.x, ray_origin.y, ray_origin.z),
239 Vector::new(ray_dir.x, ray_dir.y, ray_dir.z),
240 );
241
242 let rc = handle_world_radius(self.center, &ctx.camera, ctx.viewport_size.y, 10.0);
243 let rt = handle_world_radius(tip, &ctx.camera, ctx.viewport_size.y, 8.0);
244
245 let dc = ray_point_dist(ray_origin, ray_dir, self.center);
246 let dt = ray_point_dist(ray_origin, ray_dir, tip);
247
248 let center_ball = parry3d::shape::Ball::new(rc);
249 let tip_ball = parry3d::shape::Ball::new(rt);
250 let center_pose = Pose::from_parts(
251 [self.center.x, self.center.y, self.center.z].into(),
252 glam::Quat::IDENTITY,
253 );
254 let tip_pose = Pose::from_parts([tip.x, tip.y, tip.z].into(), glam::Quat::IDENTITY);
255
256 let tc = if dc < rc {
257 center_ball.cast_ray(¢er_pose, &ray, f32::MAX, true)
258 } else {
259 None
260 };
261 let tt = if dt < rt {
262 tip_ball.cast_ray(&tip_pose, &ray, f32::MAX, true)
263 } else {
264 None
265 };
266
267 match (tc, tt) {
268 (Some(a), Some(b)) => Some(if a <= b {
269 PlaneHandle::Center
270 } else {
271 PlaneHandle::NormalTip
272 }),
273 (Some(_), None) => Some(PlaneHandle::Center),
274 (None, Some(_)) => Some(PlaneHandle::NormalTip),
275 (None, None) => None,
276 }
277 }
278}