1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::World;
3use crate::engine::ecs::component::{
4 RayCastComponent, RayCastMode, RaycastableComponent, RaycastableShapeComponent,
5 RaycastableShapeType, RenderableComponent,
6};
7use crate::engine::ecs::system::BvhSystem;
8use crate::engine::ecs::system::System;
9use crate::engine::ecs::system::TransformSystem;
10use crate::engine::ecs::{EventSignal, RxWorld};
11use crate::engine::graphics::VisualWorld;
12use crate::engine::graphics::primitives::{CpuMeshHandle, TransformMatrix};
13use crate::engine::user_input::InputState;
14use crate::utils::math;
15use std::collections::{HashMap, HashSet};
16use std::sync::OnceLock;
17use std::time::{Duration, Instant};
18use winit::event::MouseButton;
19
20#[derive(Debug, Default)]
21pub struct RayCastSystem {
22 raycasters: HashSet<ComponentId>,
23 last_hit: HashMap<ComponentId, Option<ComponentId>>,
24
25 eligible_renderables: HashSet<ComponentId>,
29 profile_frames: u64,
30 profile_rays: u64,
31 profile_bvh_hits: u64,
32 profile_fallbacks: u64,
33 profile_fallback_candidates: u64,
34 profile_query_time: Duration,
35}
36
37#[derive(Debug, Clone, Copy)]
38struct CursorRay {
39 origin: [f32; 3],
40 dir: [f32; 3],
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44enum RaySourceKind {
45 CursorThroughActiveCamera,
46 ParentForward,
47}
48
49use crate::engine::ecs::system::pointer_system::pointer_topology_context;
51
52impl RayCastSystem {
53 fn debug_raycast_enabled() -> bool {
54 static ENABLED: OnceLock<bool> = OnceLock::new();
55 *ENABLED.get_or_init(|| {
56 let v = std::env::var("CAT_DEBUG_RAYCAST").unwrap_or_default();
57 matches!(
58 v.trim().to_ascii_lowercase().as_str(),
59 "1" | "true" | "yes" | "on"
60 )
61 })
62 }
63
64 fn profile_spatial_enabled() -> bool {
65 static ENABLED: OnceLock<bool> = OnceLock::new();
66 *ENABLED.get_or_init(|| {
67 matches!(
68 std::env::var("CAT_PROFILE_SPATIAL")
69 .unwrap_or_default()
70 .trim()
71 .to_ascii_lowercase()
72 .as_str(),
73 "1" | "true" | "yes" | "on"
74 )
75 })
76 }
77
78 fn debug_component_label(world: &World, component: ComponentId) -> String {
79 world
80 .get_component_record(component)
81 .map(|n| {
82 if n.name.is_empty() {
83 n.component_type.clone()
84 } else {
85 format!("{}: {}", n.component_type, n.name)
86 }
87 })
88 .unwrap_or_else(|| "<missing>".to_string())
89 }
90
91 fn raycastable_for_renderable(
92 world: &World,
93 renderable: ComponentId,
94 ) -> Option<RaycastableComponent> {
95 BvhSystem::find_raycastable_for_renderable(world, renderable)
96 }
97
98 fn sort_hits_by_priority(world: &World, hits: &mut [(ComponentId, f32)]) {
99 hits.sort_by(|a, b| {
100 let a_pri = Self::raycastable_for_renderable(world, a.0)
101 .map(|rc| rc.interaction_priority)
102 .unwrap_or(0);
103 let b_pri = Self::raycastable_for_renderable(world, b.0)
104 .map(|rc| rc.interaction_priority)
105 .unwrap_or(0);
106
107 b_pri
108 .cmp(&a_pri)
109 .then_with(|| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
110 });
111 }
112
113 fn explicit_shape_on_renderable(
114 world: &World,
115 renderable: ComponentId,
116 ) -> Option<RaycastableShapeType> {
117 world.children_of(renderable).iter().find_map(|&ch| {
118 world
119 .get_component_by_id_as::<RaycastableShapeComponent>(ch)
120 .map(|s| s.shape)
121 })
122 }
123
124 fn infer_shape_from_base_mesh(mesh: CpuMeshHandle) -> RaycastableShapeType {
125 match mesh {
126 CpuMeshHandle::CUBE => RaycastableShapeType::Box,
127 CpuMeshHandle::QUAD_2D => RaycastableShapeType::Quad2D,
128 CpuMeshHandle::TRIANGLE_2D => RaycastableShapeType::Triangle2D,
129 CpuMeshHandle::TETRAHEDRON => RaycastableShapeType::Tetrahedron,
130 CpuMeshHandle::CONE => RaycastableShapeType::Cone,
131 CpuMeshHandle::CIRCLE_2D => RaycastableShapeType::Ring2D,
132 _ => RaycastableShapeType::Aabb,
133 }
134 }
135
136 fn resolved_shape_for_renderable(
137 world: &World,
138 renderable: ComponentId,
139 ) -> RaycastableShapeType {
140 if let Some(shape) = Self::explicit_shape_on_renderable(world, renderable) {
141 if shape != RaycastableShapeType::InferFromBaseMesh {
142 return shape;
143 }
144 }
145
146 let Some(r) = world.get_component_by_id_as::<RenderableComponent>(renderable) else {
147 return RaycastableShapeType::Aabb;
148 };
149 Self::infer_shape_from_base_mesh(r.renderable.base_mesh)
150 }
151
152 fn narrow_phase_accept(
153 world: &World,
154 renderable: ComponentId,
155 origin: [f32; 3],
156 dir: [f32; 3],
157 t_aabb: f32,
158 ) -> Option<f32> {
159 let shape = Self::resolved_shape_for_renderable(world, renderable);
160
161 let Some(model) = TransformSystem::world_model(world, renderable) else {
163 return Some(t_aabb);
164 };
165 let Some(inv_model) = math::mat4_inverse(model) else {
166 return Some(t_aabb);
167 };
168
169 let o4 = Self::mat4_mul_vec4(inv_model, [origin[0], origin[1], origin[2], 1.0]);
171 let d4 = Self::mat4_mul_vec4(inv_model, [dir[0], dir[1], dir[2], 0.0]);
172 let o_local = [o4[0], o4[1], o4[2]];
173 let d_local = [d4[0], d4[1], d4[2]];
174
175 fn to_world_t(
176 model: TransformMatrix,
177 origin_world: [f32; 3],
178 dir_world: [f32; 3],
179 p_local: [f32; 3],
180 ) -> Option<f32> {
181 let w = RayCastSystem::mat4_mul_vec4(model, [p_local[0], p_local[1], p_local[2], 1.0]);
182 let p_world = [w[0], w[1], w[2]];
183 let v = RayCastSystem::vec3_sub(p_world, origin_world);
184 let t = RayCastSystem::vec3_dot(v, dir_world);
185 if t.is_finite() && t >= 0.0 {
186 Some(t)
187 } else {
188 None
189 }
190 }
191
192 fn ray_triangle_mt(
193 o: [f32; 3],
194 d: [f32; 3],
195 v0: [f32; 3],
196 v1: [f32; 3],
197 v2: [f32; 3],
198 ) -> Option<f32> {
199 let eps = 1e-7_f32;
201 let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
202 let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
203 let p = RayCastSystem::vec3_cross(d, e2);
204 let det = RayCastSystem::vec3_dot(e1, p);
205 if det.abs() < eps {
206 return None;
207 }
208 let inv_det = 1.0 / det;
209 let tvec = [o[0] - v0[0], o[1] - v0[1], o[2] - v0[2]];
210 let u = RayCastSystem::vec3_dot(tvec, p) * inv_det;
211 if !(0.0..=1.0).contains(&u) {
212 return None;
213 }
214 let q = RayCastSystem::vec3_cross(tvec, e1);
215 let v = RayCastSystem::vec3_dot(d, q) * inv_det;
216 if v < 0.0 || u + v > 1.0 {
217 return None;
218 }
219 let t = RayCastSystem::vec3_dot(e2, q) * inv_det;
220 if t.is_finite() && t >= 0.0 {
221 Some(t)
222 } else {
223 None
224 }
225 }
226
227 fn best_triangle_hit_world_t(
228 model: TransformMatrix,
229 origin_world: [f32; 3],
230 dir_world: [f32; 3],
231 o_local: [f32; 3],
232 d_local: [f32; 3],
233 tris: &[[[f32; 3]; 3]],
234 ) -> Option<f32> {
235 let mut best: Option<f32> = None;
236 for tri in tris {
237 let t_local = ray_triangle_mt(o_local, d_local, tri[0], tri[1], tri[2]);
238 let Some(t_local) = t_local else {
239 continue;
240 };
241 let p_local = [
242 o_local[0] + d_local[0] * t_local,
243 o_local[1] + d_local[1] * t_local,
244 o_local[2] + d_local[2] * t_local,
245 ];
246 let Some(t_world) = to_world_t(model, origin_world, dir_world, p_local) else {
247 continue;
248 };
249 best = match best {
250 None => Some(t_world),
251 Some(bt) if t_world < bt => Some(t_world),
252 _ => best,
253 };
254 }
255 best
256 }
257
258 match shape {
260 RaycastableShapeType::Aabb => Some(t_aabb),
261
262 RaycastableShapeType::Box => {
263 let t_local =
264 Self::ray_aabb(o_local, d_local, [-0.5, -0.5, -0.5], [0.5, 0.5, 0.5])?;
265 let p_local = [
266 o_local[0] + d_local[0] * t_local,
267 o_local[1] + d_local[1] * t_local,
268 o_local[2] + d_local[2] * t_local,
269 ];
270 to_world_t(model, origin, dir, p_local)
271 }
272
273 RaycastableShapeType::Quad2D => {
274 let thick = 0.02_f32;
276 let t_local =
277 Self::ray_aabb(o_local, d_local, [-0.5, -0.5, -thick], [0.5, 0.5, thick])?;
278 let p_local = [
279 o_local[0] + d_local[0] * t_local,
280 o_local[1] + d_local[1] * t_local,
281 o_local[2] + d_local[2] * t_local,
282 ];
283 to_world_t(model, origin, dir, p_local)
284 }
285
286 RaycastableShapeType::Triangle2D => {
287 let h = 0.866_025_4_f32;
289 let y_top = 2.0 * h / 3.0;
290 let y_bottom = -h / 3.0;
291 let v0 = [-0.5, y_bottom, 0.0];
292 let v1 = [0.5, y_bottom, 0.0];
293 let v2 = [0.0, y_top, 0.0];
294 let tris: [[[f32; 3]; 3]; 1] = [[v0, v1, v2]];
295 best_triangle_hit_world_t(model, origin, dir, o_local, d_local, &tris[..])
296 }
297
298 RaycastableShapeType::Ring2D => {
299 let mut r_in = 0.45_f32;
302 let mut r_out = 0.5_f32;
303
304 let tol = 0.03_f32;
306 r_in = (r_in - tol).max(0.0);
307 r_out += tol;
308
309 let dz = d_local[2];
311 if dz.abs() < 1e-6 {
312 return None;
313 }
314 let t_local = -o_local[2] / dz;
315 if !t_local.is_finite() || t_local < 0.0 {
316 return None;
317 }
318
319 let p_local = [
320 o_local[0] + d_local[0] * t_local,
321 o_local[1] + d_local[1] * t_local,
322 0.0,
323 ];
324 let r = (p_local[0] * p_local[0] + p_local[1] * p_local[1]).sqrt();
325 if r < r_in || r > r_out {
326 return None;
327 }
328 to_world_t(model, origin, dir, p_local)
329 }
330
331 RaycastableShapeType::Cone => {
332 let r = 0.5_f32;
335 let zmin = -0.5_f32;
336 let zmax = 0.5_f32;
337
338 let ox = o_local[0];
339 let oy = o_local[1];
340 let oz = o_local[2];
341 let dx = d_local[0];
342 let dy = d_local[1];
343 let dz = d_local[2];
344
345 let mut best_t: Option<f32> = None;
346
347 let a = dx * dx + dy * dy;
349 let b = 2.0 * (ox * dx + oy * dy);
350 let c = ox * ox + oy * oy - r * r;
351
352 if a.abs() > 1e-8 {
353 let disc = b * b - 4.0 * a * c;
354 if disc >= 0.0 {
355 let s = disc.sqrt();
356 let t0 = (-b - s) / (2.0 * a);
357 let t1 = (-b + s) / (2.0 * a);
358 for t in [t0, t1] {
359 if !t.is_finite() || t < 0.0 {
360 continue;
361 }
362 let z = oz + dz * t;
363 if z >= zmin && z <= zmax {
364 best_t = match best_t {
365 None => Some(t),
366 Some(bt) if t < bt => Some(t),
367 _ => best_t,
368 };
369 }
370 }
371 }
372 }
373
374 if dz.abs() > 1e-8 {
376 for z_plane in [zmin, zmax] {
377 let t = (z_plane - oz) / dz;
378 if !t.is_finite() || t < 0.0 {
379 continue;
380 }
381 let x = ox + dx * t;
382 let y = oy + dy * t;
383 if x * x + y * y <= r * r {
384 best_t = match best_t {
385 None => Some(t),
386 Some(bt) if t < bt => Some(t),
387 _ => best_t,
388 };
389 }
390 }
391 }
392
393 let t_local = best_t?;
394 let p_local = [ox + dx * t_local, oy + dy * t_local, oz + dz * t_local];
395 to_world_t(model, origin, dir, p_local)
396 }
397
398 RaycastableShapeType::Tetrahedron => {
399 let p0 = [0.0, 0.0, 0.6123724];
401 let p1 = [-0.5, -0.2886751, -0.2041241];
402 let p2 = [0.5, -0.2886751, -0.2041241];
403 let p3 = [0.0, 0.5773503, -0.2041241];
404
405 let tris = [[p0, p1, p2], [p0, p3, p1], [p0, p2, p3], [p1, p3, p2]];
407 best_triangle_hit_world_t(model, origin, dir, o_local, d_local, &tris[..])
408 }
409
410 RaycastableShapeType::InferFromBaseMesh => Some(t_aabb),
411 }
412 }
413 pub fn register_raycast(
414 &mut self,
415 world: &mut World,
416 _visuals: &mut VisualWorld,
417 component: ComponentId,
418 ) {
419 if world
420 .get_component_by_id_as::<RayCastComponent>(component)
421 .is_none()
422 {
423 return;
424 }
425 self.raycasters.insert(component);
426 }
427
428 pub fn remove_raycast(
429 &mut self,
430 _world: &mut World,
431 _visuals: &mut VisualWorld,
432 component: ComponentId,
433 ) {
434 self.raycasters.remove(&component);
435 self.last_hit.remove(&component);
436 }
437
438 pub fn notify_renderable_added(&mut self, world: &World, renderable_cid: ComponentId) {
439 if BvhSystem::renderable_is_raycastable(world, renderable_cid) {
440 self.eligible_renderables.insert(renderable_cid);
441 } else {
442 self.eligible_renderables.remove(&renderable_cid);
443 }
444 }
445
446 pub fn notify_renderable_removed(&mut self, renderable_cid: ComponentId) {
447 self.eligible_renderables.remove(&renderable_cid);
448 }
449
450 fn should_cast(
451 mode: RayCastMode,
452 input: &InputState,
453 cast_requested: bool,
454 source: RaySourceKind,
455 pointer_trigger_active: bool,
456 ) -> bool {
457 match mode {
458 RayCastMode::Continuous => true,
459 RayCastMode::EventDriven => {
460 let desktop_mouse_auto_cast = source == RaySourceKind::CursorThroughActiveCamera
461 && (input.mouse_pressed.contains(&MouseButton::Left)
462 || (input.mouse_down.contains(&MouseButton::Left)
463 && input.mouse_dragging()));
464
465 cast_requested || desktop_mouse_auto_cast || pointer_trigger_active
466 }
467 }
468 }
469
470 fn mat4_mul(a: TransformMatrix, b: TransformMatrix) -> TransformMatrix {
471 let mut out = [[0.0f32; 4]; 4];
473 for c in 0..4 {
474 for r in 0..4 {
475 out[c][r] =
476 a[0][r] * b[c][0] + a[1][r] * b[c][1] + a[2][r] * b[c][2] + a[3][r] * b[c][3];
477 }
478 }
479 out
480 }
481
482 fn mat4_mul_vec4(m: TransformMatrix, v: [f32; 4]) -> [f32; 4] {
483 [
485 m[0][0] * v[0] + m[1][0] * v[1] + m[2][0] * v[2] + m[3][0] * v[3],
486 m[0][1] * v[0] + m[1][1] * v[1] + m[2][1] * v[2] + m[3][1] * v[3],
487 m[0][2] * v[0] + m[1][2] * v[1] + m[2][2] * v[2] + m[3][2] * v[3],
488 m[0][3] * v[0] + m[1][3] * v[1] + m[2][3] * v[2] + m[3][3] * v[3],
489 ]
490 }
491
492 fn vec3_sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
493 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
494 }
495
496 fn vec3_mul_scalar(v: [f32; 3], s: f32) -> [f32; 3] {
497 [v[0] * s, v[1] * s, v[2] * s]
498 }
499
500 fn vec3_dot(a: [f32; 3], b: [f32; 3]) -> f32 {
501 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
502 }
503
504 fn vec3_len(v: [f32; 3]) -> f32 {
505 Self::vec3_dot(v, v).sqrt()
506 }
507
508 fn vec3_normalize(v: [f32; 3]) -> [f32; 3] {
509 let len = Self::vec3_len(v);
510 if len > 0.0 {
511 Self::vec3_mul_scalar(v, 1.0 / len)
512 } else {
513 [0.0, 0.0, 1.0]
514 }
515 }
516
517 fn ray_from_cursor(visuals: &VisualWorld, input: &InputState) -> Option<CursorRay> {
518 let vp = visuals.viewport();
519 let w = vp[0];
520 let h = vp[1];
521 if w <= 0.0 || h <= 0.0 {
522 return None;
523 }
524
525 let (cx, cy) = input.cursor_pos.unwrap_or((w * 0.5, h * 0.5));
526
527 let x_ndc = (2.0 * (cx / w)) - 1.0;
529 let y_ndc = 1.0 - (2.0 * (cy / h));
530
531 let view = visuals.camera_view();
532 let proj = visuals.camera_proj();
533 let vp_mat = Self::mat4_mul(proj, view);
534 let inv_vp = math::mat4_inverse(vp_mat)?;
535
536 let near_clip = [x_ndc, y_ndc, 0.0, 1.0];
537 let far_clip = [x_ndc, y_ndc, 1.0, 1.0];
538
539 let near_world4 = Self::mat4_mul_vec4(inv_vp, near_clip);
540 let far_world4 = Self::mat4_mul_vec4(inv_vp, far_clip);
541
542 let near_w = near_world4[3];
543 let far_w = far_world4[3];
544 if near_w == 0.0 || far_w == 0.0 {
545 return None;
546 }
547
548 let near = [
549 near_world4[0] / near_w,
550 near_world4[1] / near_w,
551 near_world4[2] / near_w,
552 ];
553 let far = [
554 far_world4[0] / far_w,
555 far_world4[1] / far_w,
556 far_world4[2] / far_w,
557 ];
558
559 let dir = Self::vec3_normalize(Self::vec3_sub(far, near));
560 Some(CursorRay { origin: near, dir })
561 }
562
563 fn mat4_mul_vec3_dir(m: TransformMatrix, v: [f32; 3]) -> [f32; 3] {
564 let w = Self::mat4_mul_vec4(m, [v[0], v[1], v[2], 0.0]);
566 [w[0], w[1], w[2]]
567 }
568
569 fn vec3_cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
570 [
571 a[1] * b[2] - a[2] * b[1],
572 a[2] * b[0] - a[0] * b[2],
573 a[0] * b[1] - a[1] * b[0],
574 ]
575 }
576
577 fn inferred_source_kind(world: &World, raycaster_cid: ComponentId) -> RaySourceKind {
587 let topology = pointer_topology_context(world, raycaster_cid);
588
589 let _future_trigger_policy_hint = (
590 topology.has_desktop_input_driver,
591 topology.has_xr_input_driver,
592 topology.has_controller_driver,
593 topology.has_xr_camera_anchor,
594 );
595
596 if topology.has_desktop_camera_anchor {
597 RaySourceKind::CursorThroughActiveCamera
598 } else {
599 RaySourceKind::ParentForward
600 }
601 }
602
603 fn ray_from_parent_forward(
604 world: &World,
605 raycaster_cid: ComponentId,
606 ) -> Option<([f32; 3], [f32; 3])> {
607 let model = TransformSystem::world_model(world, raycaster_cid)?;
609 let origin = [model[3][0], model[3][1], model[3][2]];
610
611 let forward_local = [0.0, 0.0, -1.0];
613 let dir_world = Self::vec3_normalize(Self::mat4_mul_vec3_dir(model, forward_local));
614 Some((origin, dir_world))
615 }
616
617 fn aabb_from_world_matrix_for_mesh(
618 mesh: CpuMeshHandle,
619 m: TransformMatrix,
620 ) -> Option<([f32; 3], [f32; 3])> {
621 let (local_pts, thickness) = match mesh {
622 CpuMeshHandle::CUBE => {
623 (
625 vec![
626 [-0.5, -0.5, -0.5],
627 [0.5, -0.5, -0.5],
628 [-0.5, 0.5, -0.5],
629 [0.5, 0.5, -0.5],
630 [-0.5, -0.5, 0.5],
631 [0.5, -0.5, 0.5],
632 [-0.5, 0.5, 0.5],
633 [0.5, 0.5, 0.5],
634 ],
635 0.0,
636 )
637 }
638 CpuMeshHandle::TETRAHEDRON => (
639 vec![
640 [0.0, 0.0, 0.6123724],
641 [-0.5, -0.2886751, -0.2041241],
642 [0.5, -0.2886751, -0.2041241],
643 [0.0, 0.5773503, -0.2041241],
644 ],
645 0.0,
646 ),
647 CpuMeshHandle::CONE => (
648 vec![
649 [-0.5, 0.0, -0.5],
651 [0.5, 0.0, -0.5],
652 [0.0, -0.5, -0.5],
653 [0.0, 0.5, -0.5],
654 [0.0, 0.0, 0.5],
655 ],
656 0.0,
657 ),
658 CpuMeshHandle::QUAD_2D | CpuMeshHandle::TRIANGLE_2D | CpuMeshHandle::CIRCLE_2D => {
659 (
661 vec![
662 [-0.5, -0.5, 0.0],
663 [0.5, -0.5, 0.0],
664 [-0.5, 0.5, 0.0],
665 [0.5, 0.5, 0.0],
666 ],
667 0.01,
668 )
669 }
670 CpuMeshHandle::SPHERE => (
671 vec![
672 [-0.5, 0.0, 0.0],
673 [0.5, 0.0, 0.0],
674 [0.0, -0.5, 0.0],
675 [0.0, 0.5, 0.0],
676 [0.0, 0.0, -0.5],
677 [0.0, 0.0, 0.5],
678 ],
679 0.0,
680 ),
681 _ => return None,
682 };
683
684 let mut min = [f32::INFINITY; 3];
685 let mut max = [f32::NEG_INFINITY; 3];
686
687 for p in local_pts {
688 let v = [p[0], p[1], p[2], 1.0];
689 let w = Self::mat4_mul_vec4(m, v);
690 let wp = [w[0], w[1], w[2]];
691 for i in 0..3 {
692 min[i] = min[i].min(wp[i]);
693 max[i] = max[i].max(wp[i]);
694 }
695 }
696
697 if thickness > 0.0 {
698 min[2] -= thickness;
699 max[2] += thickness;
700 }
701
702 Some((min, max))
703 }
704
705 fn ray_aabb(
706 origin: [f32; 3],
707 dir: [f32; 3],
708 aabb_min: [f32; 3],
709 aabb_max: [f32; 3],
710 ) -> Option<f32> {
711 let mut tmin = 0.0f32;
713 let mut tmax = f32::INFINITY;
714
715 for axis in 0..3 {
716 let o = origin[axis];
717 let d = dir[axis];
718 let min = aabb_min[axis];
719 let max = aabb_max[axis];
720
721 if d.abs() < 1e-6 {
722 if o < min || o > max {
723 return None;
724 }
725 continue;
726 }
727
728 let inv_d = 1.0 / d;
729 let mut t0 = (min - o) * inv_d;
730 let mut t1 = (max - o) * inv_d;
731 if t0 > t1 {
732 std::mem::swap(&mut t0, &mut t1);
733 }
734 tmin = tmin.max(t0);
735 tmax = tmax.min(t1);
736 if tmax < tmin {
737 return None;
738 }
739 }
740
741 if tmin >= 0.0 {
742 Some(tmin)
743 } else if tmax >= 0.0 {
744 Some(tmax)
745 } else {
746 None
747 }
748 }
749
750 fn cast_against_renderables(
752 &self,
753 world: &World,
754 origin: [f32; 3],
755 dir: [f32; 3],
756 max_distance: f32,
757 ) -> Vec<(ComponentId, f32)> {
758 let mut hits: Vec<(ComponentId, f32)> = Vec::new();
759
760 for &cid in self.eligible_renderables.iter() {
761 let Some(r) = world.get_component_by_id_as::<RenderableComponent>(cid) else {
762 continue;
763 };
764
765 let mesh = r.renderable.base_mesh;
766 let Some(model) = TransformSystem::world_model(world, cid) else {
767 continue;
768 };
769
770 let Some((min, max)) = Self::aabb_from_world_matrix_for_mesh(mesh, model) else {
771 continue;
772 };
773
774 let Some(t) = Self::ray_aabb(origin, dir, min, max) else {
775 continue;
776 };
777
778 if t < 0.0 || t > max_distance {
779 continue;
780 }
781
782 let Some(t2) = Self::narrow_phase_accept(world, cid, origin, dir, t) else {
783 continue;
784 };
785 if t2 < 0.0 || t2 > max_distance {
786 continue;
787 }
788
789 hits.push((cid, t2));
790 }
791
792 Self::sort_hits_by_priority(world, &mut hits);
793 hits
794 }
795
796 fn cast_against_renderables_bvh(
799 &self,
800 world: &World,
801 bvh: &BvhSystem,
802 origin: [f32; 3],
803 dir: [f32; 3],
804 max_distance: f32,
805 ) -> Vec<(ComponentId, f32)> {
806 let candidates = bvh.raycast_renderables_candidates(origin, dir, max_distance, 64);
807
808 let mut hits: Vec<(ComponentId, f32)> = Vec::new();
809 for (cid, t_aabb) in candidates {
810 if world
811 .get_component_by_id_as::<RenderableComponent>(cid)
812 .is_none()
813 {
814 continue;
815 }
816
817 let Some(t2) = Self::narrow_phase_accept(world, cid, origin, dir, t_aabb) else {
818 continue;
819 };
820 if t2 < 0.0 || t2 > max_distance {
821 continue;
822 }
823
824 hits.push((cid, t2));
825 }
826
827 Self::sort_hits_by_priority(world, &mut hits);
828 hits
829 }
830}
831
832impl System for RayCastSystem {
833 fn tick(
834 &mut self,
835 world: &mut World,
836 visuals: &mut VisualWorld,
837 input: &InputState,
838 _dt_sec: f32,
839 ) {
840 if self.raycasters.is_empty() {
845 return;
846 }
847
848 let Some(ray) = Self::ray_from_cursor(visuals, input) else {
849 return;
850 };
851
852 let raycasters: Vec<ComponentId> = self.raycasters.iter().copied().collect();
854 for rcid in raycasters {
855 let Some(rc) = world.get_component_by_id_as::<RayCastComponent>(rcid) else {
856 self.raycasters.remove(&rcid);
857 self.last_hit.remove(&rcid);
858 continue;
859 };
860
861 let cast_requested = rc.cast_requests > 0;
862
863 let source = Self::inferred_source_kind(world, rcid);
864
865 if !Self::should_cast(rc.mode, input, cast_requested, source, false) {
866 continue;
867 }
868
869 let hits = self.cast_against_renderables(world, ray.origin, ray.dir, rc.max_distance);
870 let best = hits.first().copied();
871
872 match rc.mode {
873 RayCastMode::Continuous => {
874 let next = best.map(|(cid, _)| cid);
875 self.last_hit.insert(rcid, next);
876 }
877 RayCastMode::EventDriven => {}
878 }
879
880 if cast_requested {
881 if let Some(rc) = world.get_component_by_id_as_mut::<RayCastComponent>(rcid) {
882 rc.cast_requests = 0;
883 }
884 }
885 }
886 }
887}
888
889impl RayCastSystem {
890 pub fn tick_with_queue(
891 &mut self,
892 world: &mut World,
893 visuals: &mut VisualWorld,
894 input: &InputState,
895 rx: &mut RxWorld,
896 bvh: &BvhSystem,
897 activations: &crate::engine::ecs::system::pointer_system::PointerActivations,
898 pointer_system: &crate::engine::ecs::system::pointer_system::PointerSystem,
899 _dt_sec: f32,
900 ) {
901 let profile = Self::profile_spatial_enabled();
902 if profile {
903 self.profile_frames += 1;
904 }
905 let cursor_ray = Self::ray_from_cursor(visuals, input);
907
908 if !self.raycasters.is_empty() {
909 let raycasters: Vec<ComponentId> = self.raycasters.iter().copied().collect();
911 for rcid in raycasters {
912 let Some(rc) = world.get_component_by_id_as::<RayCastComponent>(rcid) else {
913 self.raycasters.remove(&rcid);
914 self.last_hit.remove(&rcid);
915 continue;
916 };
917
918 let mode = rc.mode;
920 let max_distance = rc.max_distance;
921 let cast_requested = rc.cast_requests > 0;
922
923 let source = Self::inferred_source_kind(world, rcid);
924
925 let pointer_trigger_active = pointer_system
927 .raycast_to_pointer(rcid)
928 .map(|ptr| {
929 activations.down.contains(&ptr) || activations.pressed.contains(&ptr)
930 })
931 .unwrap_or(false);
932
933 let (origin, dir) = match source {
934 RaySourceKind::CursorThroughActiveCamera => {
935 let Some(r) = cursor_ray else {
936 continue;
937 };
938 (r.origin, r.dir)
939 }
940 RaySourceKind::ParentForward => {
941 let Some((o, d)) = Self::ray_from_parent_forward(world, rcid) else {
942 continue;
943 };
944 (o, d)
945 }
946 };
947
948 if !Self::should_cast(mode, input, cast_requested, source, pointer_trigger_active) {
949 continue;
950 }
951
952 let query_started = profile.then(Instant::now);
953 let mut hits =
954 self.cast_against_renderables_bvh(world, bvh, origin, dir, max_distance);
955 if profile {
956 self.profile_rays += 1;
957 self.profile_bvh_hits += hits.len() as u64;
958 }
959 if hits.is_empty() && !bvh.has_index() {
960 hits = self.cast_against_renderables(world, origin, dir, max_distance);
961 if profile {
962 self.profile_fallbacks += 1;
963 self.profile_fallback_candidates += self.eligible_renderables.len() as u64;
964 }
965 }
966 if let Some(started) = query_started {
967 self.profile_query_time += started.elapsed();
968 }
969 if Self::debug_raycast_enabled() {
970 let summary: Vec<String> = hits
971 .iter()
972 .take(8)
973 .map(|(cid, t)| {
974 let rc = Self::raycastable_for_renderable(world, *cid);
975 format!(
976 "{:?} '{}' t={:.3} pri={} pe={:?}",
977 cid,
978 Self::debug_component_label(world, *cid),
979 t,
980 rc.map(|r| r.interaction_priority).unwrap_or(0),
981 rc.map(|r| r.pointer_events)
982 .unwrap_or(crate::engine::ecs::component::PointerEvents::All)
983 )
984 })
985 .collect();
986 eprintln!(
987 "[raycast] rc={:?} source={:?} origin=[{:+.3},{:+.3},{:+.3}] dir=[{:+.3},{:+.3},{:+.3}] hits={}",
988 rcid,
989 source,
990 origin[0],
991 origin[1],
992 origin[2],
993 dir[0],
994 dir[1],
995 dir[2],
996 if summary.is_empty() {
997 "<none>".to_string()
998 } else {
999 summary.join(" | ")
1000 }
1001 );
1002 }
1003
1004 for &(hit_cid, t) in &hits {
1007 rx.push_event(
1008 hit_cid,
1009 EventSignal::RayIntersected {
1010 raycaster: rcid,
1011 renderable: hit_cid,
1012 t,
1013 origin,
1014 dir,
1015 },
1016 );
1017 }
1018
1019 let best = hits.first().copied();
1020 match mode {
1021 RayCastMode::Continuous => {
1022 let next = best.map(|(cid, _)| cid);
1023 self.last_hit.insert(rcid, next);
1024 }
1025 RayCastMode::EventDriven => {}
1026 }
1027
1028 if cast_requested {
1029 if let Some(rc) = world.get_component_by_id_as_mut::<RayCastComponent>(rcid) {
1030 rc.cast_requests = 0;
1031 }
1032 }
1033 }
1034 }
1035
1036 if profile && self.profile_frames >= 120 {
1037 let line = format!(
1038 "[spatial-profile][raycast] frames={} rays={} bvh_hits={} fallbacks={} fallback_scan_candidates={} query_ms={:.3}",
1039 self.profile_frames,
1040 self.profile_rays,
1041 self.profile_bvh_hits,
1042 self.profile_fallbacks,
1043 self.profile_fallback_candidates,
1044 self.profile_query_time.as_secs_f64() * 1000.0
1045 );
1046 eprintln!("{line}");
1047 crate::utils::profile_log::append("spatial-profile", &line);
1048 self.profile_frames = 0;
1049 self.profile_rays = 0;
1050 self.profile_bvh_hits = 0;
1051 self.profile_fallbacks = 0;
1052 self.profile_fallback_candidates = 0;
1053 self.profile_query_time = Duration::ZERO;
1054 }
1055 }
1056}