1use crate::engine::ecs::component::PointerEvents;
2use crate::engine::ecs::system::BvhSystem;
3use crate::engine::ecs::system::pointer_system::{PointerActivations, PointerSystem};
4use crate::engine::ecs::{ComponentId, EventSignal, RxWorld, SignalKind};
5use crate::engine::graphics::VisualWorld;
6use crate::engine::user_input::InputState;
7use crate::utils::math;
8use std::collections::HashMap;
9use std::sync::OnceLock;
10use std::sync::{Arc, Mutex};
11
12#[derive(Debug, Copy, Clone, PartialEq, Eq)]
13pub enum DragUpdatePolicy {
14 RequireTargetContact,
16
17 StartPlaneProjection,
22}
23
24const CLICK_THRESHOLD_PX: f32 = 8.0;
26const CLICK_THRESHOLD_WORLD: f32 = 0.02;
28
29#[derive(Debug, Default, Clone)]
30pub struct GestureState {
31 pub dragging: bool,
32 pub drag_raycaster: Option<ComponentId>,
33 pub drag_renderable: Option<ComponentId>,
34 pub click_renderable: Option<ComponentId>,
37 pub last_hit_point: Option<[f32; 3]>,
38
39 pub last_cursor_pos: Option<(f32, f32)>,
41 pub drag_plane_point_world: Option<[f32; 3]>,
42 pub drag_plane_normal_world: Option<[f32; 3]>,
43
44 pub drag_start_screen_pos: Option<(f32, f32)>,
46 pub drag_start_hit_point: Option<[f32; 3]>,
47}
48
49#[derive(Debug)]
50pub struct GestureSystem {
51 states: HashMap<ComponentId, GestureState>,
53 pub drag_update_policy: DragUpdatePolicy,
54
55 ray_hits_sorted: Arc<
58 Mutex<
59 Vec<(
60 u8,
61 f32,
62 ComponentId,
63 ComponentId,
64 [f32; 3],
65 [f32; 3],
66 PointerEvents,
67 )>,
68 >,
69 >,
70 immediate_handlers_installed: bool,
71}
72
73impl GestureSystem {
74 fn debug_gesture_enabled() -> bool {
75 static ENABLED: OnceLock<bool> = OnceLock::new();
76 *ENABLED.get_or_init(|| {
77 let v = std::env::var("CAT_DEBUG_GESTURE").unwrap_or_default();
78 matches!(
79 v.trim().to_ascii_lowercase().as_str(),
80 "1" | "true" | "yes" | "on"
81 )
82 })
83 }
84
85 pub fn begin_frame(&mut self) {
86 if let Ok(mut hits) = self.ray_hits_sorted.lock() {
87 hits.clear();
88 }
89 }
90
91 pub fn install_handlers(&mut self, rx: &mut RxWorld) {
93 if self.immediate_handlers_installed {
94 return;
95 }
96
97 let hits_ref = self.ray_hits_sorted.clone();
98 rx.add_global_handler_closure(SignalKind::RayIntersected, move |world, _emit, env| {
99 let Some(EventSignal::RayIntersected {
100 raycaster,
101 renderable,
102 t,
103 origin,
104 dir,
105 }) = env.event.as_ref()
106 else {
107 return;
108 };
109
110 if !t.is_finite() || *t < 0.0 {
111 return;
112 }
113
114 let (priority, pe) = BvhSystem::find_raycastable_for_renderable(world, *renderable)
115 .map(|rc| (rc.interaction_priority, rc.pointer_events))
116 .unwrap_or((0, PointerEvents::All));
117
118 let Ok(mut hits) = hits_ref.lock() else {
119 return;
120 };
121 let entry = (priority, *t, *raycaster, *renderable, *origin, *dir, pe);
122 let pos = hits.partition_point(|h| h.0 > priority || (h.0 == priority && h.1 < *t));
123 hits.insert(pos, entry);
124 });
125
126 self.immediate_handlers_installed = true;
127 }
128
129 pub fn state(&self) -> &GestureState {
132 self.states
134 .values()
135 .find(|s| s.dragging)
136 .or_else(|| self.states.values().next())
137 .unwrap_or(&EMPTY_GESTURE_STATE)
138 }
139
140 pub fn set_drag_update_policy(&mut self, policy: DragUpdatePolicy) {
141 self.drag_update_policy = policy;
142 }
143
144 fn mat4_mul(a: [[f32; 4]; 4], b: [[f32; 4]; 4]) -> [[f32; 4]; 4] {
145 let mut out = [[0.0f32; 4]; 4];
146 for c in 0..4 {
147 for r in 0..4 {
148 out[c][r] =
149 a[0][r] * b[c][0] + a[1][r] * b[c][1] + a[2][r] * b[c][2] + a[3][r] * b[c][3];
150 }
151 }
152 out
153 }
154
155 fn mat4_mul_vec4(m: [[f32; 4]; 4], v: [f32; 4]) -> [f32; 4] {
156 [
157 m[0][0] * v[0] + m[1][0] * v[1] + m[2][0] * v[2] + m[3][0] * v[3],
158 m[0][1] * v[0] + m[1][1] * v[1] + m[2][1] * v[2] + m[3][1] * v[3],
159 m[0][2] * v[0] + m[1][2] * v[1] + m[2][2] * v[2] + m[3][2] * v[3],
160 m[0][3] * v[0] + m[1][3] * v[1] + m[2][3] * v[2] + m[3][3] * v[3],
161 ]
162 }
163
164 fn vec3_dot(a: [f32; 3], b: [f32; 3]) -> f32 {
165 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
166 }
167
168 fn ray_from_cursor(visuals: &VisualWorld, input: &InputState) -> Option<([f32; 3], [f32; 3])> {
169 let vp = visuals.viewport();
170 let w = vp[0];
171 let h = vp[1];
172 if w <= 0.0 || h <= 0.0 {
173 return None;
174 }
175
176 let (cx, cy) = input.cursor_pos.unwrap_or((w * 0.5, h * 0.5));
177
178 let x_ndc = (2.0 * (cx / w)) - 1.0;
179 let y_ndc = 1.0 - (2.0 * (cy / h));
180
181 let view = visuals.camera_view();
182 let proj = visuals.camera_proj();
183 let vp_mat = Self::mat4_mul(proj, view);
184 let inv_vp = math::mat4_inverse(vp_mat)?;
185
186 let near_clip = [x_ndc, y_ndc, 0.0, 1.0];
187 let far_clip = [x_ndc, y_ndc, 1.0, 1.0];
188
189 let near_world4 = Self::mat4_mul_vec4(inv_vp, near_clip);
190 let far_world4 = Self::mat4_mul_vec4(inv_vp, far_clip);
191
192 let near_w = near_world4[3];
193 let far_w = far_world4[3];
194 if near_w == 0.0 || far_w == 0.0 {
195 return None;
196 }
197
198 let near = [
199 near_world4[0] / near_w,
200 near_world4[1] / near_w,
201 near_world4[2] / near_w,
202 ];
203 let far = [
204 far_world4[0] / far_w,
205 far_world4[1] / far_w,
206 far_world4[2] / far_w,
207 ];
208
209 let dir = math::vec3_normalize([far[0] - near[0], far[1] - near[1], far[2] - near[2]]);
210 Some((near, dir))
211 }
212
213 fn ray_plane_intersect(
214 origin: [f32; 3],
215 dir: [f32; 3],
216 plane_point: [f32; 3],
217 plane_normal: [f32; 3],
218 ) -> Option<[f32; 3]> {
219 let denom = Self::vec3_dot(plane_normal, dir);
220 if denom.abs() < 1e-6 {
221 return None;
222 }
223 let op = [
224 plane_point[0] - origin[0],
225 plane_point[1] - origin[1],
226 plane_point[2] - origin[2],
227 ];
228 let t = Self::vec3_dot(plane_normal, op) / denom;
229 if !t.is_finite() {
230 return None;
231 }
232 Some([
233 origin[0] + dir[0] * t,
234 origin[1] + dir[1] * t,
235 origin[2] + dir[2] * t,
236 ])
237 }
238
239 pub fn tick_with_rx(
244 &mut self,
245 visuals: &VisualWorld,
246 input: &InputState,
247 activations: &PointerActivations,
248 pointer_system: &PointerSystem,
249 rx: &mut RxWorld,
250 ) {
251 let hits: Vec<(
252 u8,
253 f32,
254 ComponentId,
255 ComponentId,
256 [f32; 3],
257 [f32; 3],
258 PointerEvents,
259 )> = self
260 .ray_hits_sorted
261 .lock()
262 .ok()
263 .map(|g| g.clone())
264 .unwrap_or_default();
265
266 for &pointer_cid in &activations.pressed {
268 if self
270 .states
271 .get(&pointer_cid)
272 .map(|s| s.dragging)
273 .unwrap_or(false)
274 {
275 continue;
276 }
277
278 let Some(raycaster_cid) = pointer_system.raycast_for_pointer(pointer_cid) else {
279 continue;
280 };
281
282 let pointer_hits: Vec<_> = hits.iter().filter(|h| h.2 == raycaster_cid).collect();
284
285 let drag_hit = pointer_hits.iter().find(|h| h.6.captures_drag());
286 let click_hit = pointer_hits.iter().find(|h| h.6.captures_click());
287 if Self::debug_gesture_enabled() {
288 let summary: Vec<String> = pointer_hits
289 .iter()
290 .take(8)
291 .map(|h| format!("{:?} t={:.3} pri={} pe={:?}", h.3, h.1, h.0, h.6))
292 .collect();
293 eprintln!(
294 "[gesture] press pointer={:?} raycaster={:?} drag_hit={:?} click_hit={:?} hits={}",
295 pointer_cid,
296 raycaster_cid,
297 drag_hit.map(|h| h.3),
298 click_hit.map(|h| h.3),
299 if summary.is_empty() {
300 "<none>".to_string()
301 } else {
302 summary.join(" | ")
303 }
304 );
305 }
306
307 let Some(&&(_priority, t, raycaster, renderable, origin, dir, _pe)) = drag_hit else {
308 continue;
309 };
310
311 let drag_hit_point = Some([
312 origin[0] + dir[0] * t,
313 origin[1] + dir[1] * t,
314 origin[2] + dir[2] * t,
315 ]);
316
317 let screen_pos = input.cursor_pos;
319 let is_screen_pointer = screen_pos.is_some();
320
321 let state = self.states.entry(pointer_cid).or_default();
322 state.dragging = true;
323 state.drag_raycaster = Some(raycaster);
324 state.drag_renderable = Some(renderable);
325 state.click_renderable = click_hit.map(|h| h.3);
326 state.last_hit_point = drag_hit_point;
327 state.last_cursor_pos = if is_screen_pointer { screen_pos } else { None };
328 state.drag_start_screen_pos = if is_screen_pointer { screen_pos } else { None };
329 state.drag_start_hit_point = drag_hit_point;
330
331 if self.drag_update_policy == DragUpdatePolicy::StartPlaneProjection
333 && is_screen_pointer
334 {
335 let n = math::vec3_normalize(dir);
336 state.drag_plane_point_world = drag_hit_point;
337 state.drag_plane_normal_world = Some(n);
338 if let Some(p0) = drag_hit_point {
339 state.last_hit_point = Some(p0);
340 }
341 }
342
343 if let Some(p) = drag_hit_point {
344 rx.push_event(
345 renderable,
346 EventSignal::DragStart {
347 raycaster,
348 renderable,
349 hit_point: p,
350 ray_dir_world: dir,
351 screen_pos_px: if is_screen_pointer { screen_pos } else { None },
352 },
353 );
354 }
355 }
356
357 let active_pointers: Vec<ComponentId> = self.states.keys().copied().collect();
359 for pointer_cid in active_pointers {
360 let is_down = activations.down.contains(&pointer_cid);
361 let is_released = activations.released.contains(&pointer_cid);
362
363 if is_down {
365 let (Some(active_rc), Some(active_renderable)) = ({
366 let s = self.states.get(&pointer_cid).unwrap();
367 (s.drag_raycaster, s.drag_renderable)
368 }) else {
369 self.states.remove(&pointer_cid);
370 continue;
371 };
372
373 if !self
374 .states
375 .get(&pointer_cid)
376 .map(|s| s.dragging)
377 .unwrap_or(false)
378 {
379 continue;
380 }
381
382 let pointer_hits: Vec<_> = hits.iter().filter(|h| h.2 == active_rc).collect();
383 let is_screen_pointer = self
384 .states
385 .get(&pointer_cid)
386 .and_then(|s| s.drag_start_screen_pos)
387 .is_some();
388
389 let effective_policy = if is_screen_pointer {
390 self.drag_update_policy
391 } else {
392 DragUpdatePolicy::RequireTargetContact
393 };
394
395 match effective_policy {
396 DragUpdatePolicy::RequireTargetContact => {
397 let target_hit = pointer_hits
398 .iter()
399 .find(|h| h.2 == active_rc && h.3 == active_renderable);
400 if let Some(&(_priority, t, _rc, _r, origin, dir, _pe)) =
401 target_hit.copied()
402 {
403 let cur = [
404 origin[0] + dir[0] * t,
405 origin[1] + dir[1] * t,
406 origin[2] + dir[2] * t,
407 ];
408 let state = self.states.get_mut(&pointer_cid).unwrap();
409 if let Some(prev) = state.last_hit_point {
410 let delta = [cur[0] - prev[0], cur[1] - prev[1], cur[2] - prev[2]];
411 if delta[0] != 0.0 || delta[1] != 0.0 || delta[2] != 0.0 {
412 let screen_pos_px = if is_screen_pointer {
413 input.cursor_pos
414 } else {
415 None
416 };
417 let screen_delta_px = if is_screen_pointer {
418 match (state.last_cursor_pos, screen_pos_px) {
419 (Some((px, py)), Some((cx, cy))) => {
420 Some((cx - px, cy - py))
421 }
422 _ => None,
423 }
424 } else {
425 None
426 };
427 rx.push_event(
428 active_renderable,
429 EventSignal::DragMove {
430 raycaster: active_rc,
431 renderable: active_renderable,
432 hit_point: cur,
433 delta_world: delta,
434 screen_pos_px,
435 screen_delta_px,
436 },
437 );
438 }
439 }
440 let state = self.states.get_mut(&pointer_cid).unwrap();
441 state.last_hit_point = Some(cur);
442 state.last_cursor_pos = input.cursor_pos;
443 }
444 }
445
446 DragUpdatePolicy::StartPlaneProjection => {
447 let Some((o, d)) = Self::ray_from_cursor(visuals, input) else {
448 if let Some(s) = self.states.get_mut(&pointer_cid) {
449 s.last_cursor_pos = input.cursor_pos;
450 }
451 continue;
452 };
453
454 let (pp, pn) = {
455 let s = self.states.get(&pointer_cid).unwrap();
456 (s.drag_plane_point_world, s.drag_plane_normal_world)
457 };
458 let (Some(pp), Some(pn)) = (pp, pn) else {
459 if let Some(s) = self.states.get_mut(&pointer_cid) {
460 s.last_cursor_pos = input.cursor_pos;
461 }
462 continue;
463 };
464
465 let Some(cur) = Self::ray_plane_intersect(o, d, pp, pn) else {
466 if let Some(s) = self.states.get_mut(&pointer_cid) {
467 s.last_cursor_pos = input.cursor_pos;
468 }
469 continue;
470 };
471
472 let state = self.states.get_mut(&pointer_cid).unwrap();
473 if let Some(prev) = state.last_hit_point {
474 let delta = [cur[0] - prev[0], cur[1] - prev[1], cur[2] - prev[2]];
475 if delta[0] != 0.0 || delta[1] != 0.0 || delta[2] != 0.0 {
476 let screen_pos_px = input.cursor_pos;
477 let screen_delta_px = match (state.last_cursor_pos, screen_pos_px) {
478 (Some((px, py)), Some((cx, cy))) => Some((cx - px, cy - py)),
479 _ => None,
480 };
481 rx.push_event(
482 active_renderable,
483 EventSignal::DragMove {
484 raycaster: active_rc,
485 renderable: active_renderable,
486 hit_point: cur,
487 delta_world: delta,
488 screen_pos_px,
489 screen_delta_px,
490 },
491 );
492 }
493 }
494 state.last_hit_point = Some(cur);
495 state.last_cursor_pos = input.cursor_pos;
496 }
497 }
498 }
499
500 if is_released {
502 if let Some(state) = self.states.get(&pointer_cid) {
503 if state.dragging {
504 if let (Some(active_rc), Some(active_renderable)) =
505 (state.drag_raycaster, state.drag_renderable)
506 {
507 rx.push_event(
508 active_renderable,
509 EventSignal::DragEnd {
510 raycaster: active_rc,
511 renderable: active_renderable,
512 hit_point: state.last_hit_point,
513 },
514 );
515
516 let is_click = match (state.drag_start_screen_pos, input.cursor_pos) {
517 (Some((sx, sy)), Some((ex, ey))) => {
518 let dx = ex - sx;
519 let dy = ey - sy;
520 (dx * dx + dy * dy).sqrt() < CLICK_THRESHOLD_PX
521 }
522 _ => match (state.drag_start_hit_point, state.last_hit_point) {
523 (Some(s), Some(e)) => {
524 let d = [e[0] - s[0], e[1] - s[1], e[2] - s[2]];
525 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
526 < CLICK_THRESHOLD_WORLD
527 }
528 _ => false,
529 },
530 };
531
532 if is_click {
533 let click_target =
534 state.click_renderable.unwrap_or(active_renderable);
535 if Self::debug_gesture_enabled() {
536 eprintln!(
537 "[gesture] click pointer={:?} raycaster={:?} drag_renderable={:?} click_target={:?}",
538 pointer_cid, active_rc, active_renderable, click_target,
539 );
540 }
541 if let Some(start_hit) = state.drag_start_hit_point {
542 rx.push_event(
543 click_target,
544 EventSignal::Click {
545 raycaster: active_rc,
546 renderable: click_target,
547 hit_point: start_hit,
548 screen_pos_px: state.drag_start_screen_pos,
549 },
550 );
551 }
552 }
553 }
554 }
555 }
556 self.states.remove(&pointer_cid);
557 }
558 }
559 }
560}
561
562static EMPTY_GESTURE_STATE: GestureState = GestureState {
563 dragging: false,
564 drag_raycaster: None,
565 drag_renderable: None,
566 click_renderable: None,
567 last_hit_point: None,
568 last_cursor_pos: None,
569 drag_plane_point_world: None,
570 drag_plane_normal_world: None,
571 drag_start_screen_pos: None,
572 drag_start_hit_point: None,
573};
574
575impl Default for GestureSystem {
576 fn default() -> Self {
577 Self {
578 states: HashMap::new(),
579 drag_update_policy: DragUpdatePolicy::StartPlaneProjection,
580 ray_hits_sorted: Arc::new(Mutex::new(Vec::new())),
581 immediate_handlers_installed: false,
582 }
583 }
584}