1use std::{
2 cell::{Cell, Ref, RefCell},
3 cmp::Reverse,
4 rc::Rc,
5};
6
7use cranpose_core::{
8 MemoryApplier, NodeId,
9 collections::map::{HashMap, HashSet},
10};
11use cranpose_foundation::{MINIMUM_TOUCH_TARGET_SIZE, PointerEvent, PointerEventKind};
12use cranpose_ui::{LayoutNode, ModifierNodeSlices, SubcomposeLayoutNode};
13use cranpose_ui_graphics::{Point, PointerIcon, Rect, RoundedCornerShape, space::Ray};
14
15use crate::{
16 HitTestTarget, RenderScene, SceneUpdates,
17 graph::{ProjectiveTransform, RenderGraph},
18 scene_pick::ScenePick,
19};
20
21pub struct RenderDiagnostics {
23 reported_warnings: RefCell<HashSet<&'static str>>,
24 live_modifier_slice_lookup_miss_count: Cell<usize>,
25}
26
27impl RenderDiagnostics {
28 pub fn new() -> Self {
30 Self {
31 reported_warnings: RefCell::new(HashSet::default()),
32 live_modifier_slice_lookup_miss_count: Cell::new(0),
33 }
34 }
35
36 pub fn claim_warning_once(&self, key: &'static str) -> bool {
38 self.reported_warnings.borrow_mut().insert(key)
39 }
40
41 pub fn record_live_modifier_slice_lookup_miss(&self) {
44 self.live_modifier_slice_lookup_miss_count.set(
45 self.live_modifier_slice_lookup_miss_count
46 .get()
47 .saturating_add(1),
48 );
49 }
50
51 pub fn live_modifier_slice_lookup_miss_count(&self) -> usize {
54 self.live_modifier_slice_lookup_miss_count.get()
55 }
56}
57
58impl Default for RenderDiagnostics {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct HitClip {
67 pub quad: [[f32; 2]; 4],
69 pub bounds: Rect,
71}
72
73pub struct SurfaceTarget {
76 pub pick: Rc<ScenePick>,
78 pub index: usize,
80 pub outer_clip_bounds: Option<Rect>,
82 pub outer_clips: Vec<HitClip>,
84}
85
86const WINDOW_BOUNDS_STEPS: u16 = 2;
90
91impl SurfaceTarget {
92 pub fn window_bounds(
97 &self,
98 content_to_local: &ProjectiveTransform,
99 local: Rect,
100 ) -> Option<Rect> {
101 let local_to_content = content_to_local.inverse()?;
102 let steps = f32::from(WINDOW_BOUNDS_STEPS);
103 let grid = (0..=WINDOW_BOUNDS_STEPS)
104 .flat_map(|row| (0..=WINDOW_BOUNDS_STEPS).map(move |column| (row, column)));
105 grid.filter_map(|(row, column)| {
106 let point = Point::new(
107 local.x + local.width * f32::from(column) / steps,
108 local.y + local.height * f32::from(row) / steps,
109 );
110 let content = local_to_content.map_point(point);
111 self.pick.window_point(self.index, content)
112 })
113 .map(|point| Rect {
114 x: point.x,
115 y: point.y,
116 width: 0.0,
117 height: 0.0,
118 })
119 .reduce(|bounds, point| bounds.union(point))
120 }
121}
122
123#[derive(Clone, Copy)]
125pub struct SurfaceGeometry<'a> {
126 pub target: &'a Rc<SurfaceTarget>,
128 pub content_to_local: ProjectiveTransform,
130 pub clip_bounds: Option<Rect>,
133 pub clips: &'a [HitClip],
135}
136
137impl SurfaceGeometry<'_> {
138 pub fn window_bounds(&self, local: Rect) -> Option<Rect> {
140 self.target.window_bounds(&self.content_to_local, local)
141 }
142}
143
144#[derive(Clone, Copy)]
146pub struct HitGeometry<'a> {
147 pub rect: Rect,
149 pub quad: [[f32; 2]; 4],
151 pub local_bounds: Rect,
153 pub world_to_local: ProjectiveTransform,
155 pub hit_clip_bounds: Option<Rect>,
157 pub hit_clips: &'a [HitClip],
159 pub surface: Option<SurfaceGeometry<'a>>,
163}
164
165pub struct HitTargetSpec<'a> {
169 pub shape: Option<RoundedCornerShape>,
172 pub handlers: &'a Rc<ModifierNodeSlices>,
174}
175
176#[derive(Clone)]
178pub struct HitRegion {
179 pub node_id: NodeId,
181 pub capture_path: Vec<NodeId>,
184 pub rect: Rect,
186 pub quad: [[f32; 2]; 4],
188 pub local_bounds: Rect,
190 pub world_to_local: ProjectiveTransform,
192 pub shape: Option<RoundedCornerShape>,
194 pub handlers: Rc<ModifierNodeSlices>,
197 pub z_index: usize,
199 pub hit_clip_bounds: Option<Rect>,
201 pub hit_clips: Vec<HitClip>,
203 surface: Option<Rc<SurfaceRegion>>,
206 diagnostics: Rc<RenderDiagnostics>,
207}
208
209struct SurfaceRegion {
211 target: Rc<SurfaceTarget>,
212 content_to_local: ProjectiveTransform,
213 clip_bounds: Option<Rect>,
214 clips: Vec<HitClip>,
215}
216
217impl SurfaceRegion {
218 fn local_point(&self, window: Point) -> Option<Point> {
221 let target = &self.target;
222 if target
223 .outer_clip_bounds
224 .is_some_and(|clip| !clip.contains(window.x, window.y))
225 || !target
226 .outer_clips
227 .iter()
228 .all(|clip| point_in_quad(window, clip.quad))
229 {
230 return None;
231 }
232 self.local_in(target.pick.content_point(window, target.index)?)
233 }
234
235 fn local_point_along(&self, ray: Ray) -> Option<Point> {
239 let (index, hit) = self.target.pick.nearest_along(ray)?;
240 (index == self.target.index)
241 .then(|| self.local_in(hit.point))
242 .flatten()
243 }
244
245 fn local_in(&self, content: Point) -> Option<Point> {
248 if self
249 .clip_bounds
250 .is_some_and(|clip| !clip.contains(content.x, content.y))
251 || !self
252 .clips
253 .iter()
254 .all(|clip| point_in_quad(content, clip.quad))
255 {
256 return None;
257 }
258 self.content_to_local.map_point_in_front(content)
259 }
260
261 fn drag_point(&self, event: &PointerEvent) -> Option<Point> {
264 let target = &self.target;
265 let content = match event.ray {
266 Some(ray) => target.pick.drag_point_along(ray, target.index)?,
267 None => target
268 .pick
269 .drag_point(event.global_position, target.index)?,
270 };
271 Some(self.content_to_local.map_point(content))
272 }
273}
274
275struct HitRegionInit<'a> {
276 node_id: NodeId,
277 capture_path: Vec<NodeId>,
278 geometry: HitGeometry<'a>,
279 clip_buffer: Vec<HitClip>,
280 shape: Option<RoundedCornerShape>,
281 handlers: Rc<ModifierNodeSlices>,
282 z_index: usize,
283 diagnostics: Rc<RenderDiagnostics>,
284}
285
286impl Default for HitRegionInit<'_> {
287 fn default() -> Self {
288 Self {
289 node_id: NodeId(0),
290 capture_path: Vec::new(),
291 geometry: HitGeometry {
292 rect: Rect {
293 x: 0.0,
294 y: 0.0,
295 width: 0.0,
296 height: 0.0,
297 },
298 quad: [[0.0, 0.0]; 4],
299 local_bounds: Rect {
300 x: 0.0,
301 y: 0.0,
302 width: 0.0,
303 height: 0.0,
304 },
305 world_to_local: ProjectiveTransform::identity(),
306 hit_clip_bounds: None,
307 hit_clips: &[],
308 surface: None,
309 },
310 clip_buffer: Vec::new(),
311 shape: None,
312 handlers: Rc::default(),
313 z_index: 0,
314 diagnostics: Rc::new(RenderDiagnostics::new()),
315 }
316 }
317}
318
319impl HitRegion {
320 fn with_diagnostics(init: HitRegionInit<'_>) -> Self {
321 let HitRegionInit {
322 node_id,
323 capture_path,
324 geometry,
325 clip_buffer: mut hit_clips,
326 shape,
327 handlers,
328 z_index,
329 diagnostics,
330 } = init;
331 let HitGeometry {
332 rect,
333 quad,
334 local_bounds,
335 world_to_local,
336 hit_clip_bounds,
337 hit_clips: clips,
338 surface,
339 } = geometry;
340 hit_clips.extend_from_slice(clips);
341 Self {
342 node_id,
343 capture_path,
344 rect,
345 quad,
346 local_bounds,
347 world_to_local,
348 shape,
349 handlers,
350 z_index,
351 hit_clip_bounds,
352 hit_clips,
353 surface: surface.map(|surface| {
354 Rc::new(SurfaceRegion {
355 target: Rc::clone(surface.target),
356 content_to_local: surface.content_to_local,
357 clip_bounds: surface.clip_bounds,
358 clips: surface.clips.to_vec(),
359 })
360 }),
361 diagnostics,
362 }
363 }
364
365 fn contains(&self, x: f32, y: f32) -> bool {
366 if let Some(surface) = &self.surface {
367 return surface
368 .local_point(Point { x, y })
369 .is_some_and(|local| self.contains_local(local));
370 }
371 if !self.rect.contains(x, y) {
372 return false;
373 }
374
375 if let Some(clip_bounds) = self.hit_clip_bounds
376 && !clip_bounds.contains(x, y)
377 {
378 return false;
379 }
380
381 let point = Point { x, y };
382 if !point_in_quad(point, self.quad) {
383 return false;
384 }
385
386 for clip in &self.hit_clips {
387 if !point_in_quad(point, clip.quad) {
388 return false;
389 }
390 }
391
392 let Some(local_point) = self.world_to_local.map_point_in_front(point) else {
393 return false;
394 };
395 self.contains_local(local_point)
396 }
397
398 fn contains_local(&self, local_point: Point) -> bool {
399 if let Some(shape) = self.shape {
400 point_in_rounded_rect(local_point, self.local_bounds, shape)
401 } else {
402 self.local_bounds.contains(local_point.x, local_point.y)
403 }
404 }
405
406 fn reach_distance(&self, x: f32, y: f32) -> Option<f32> {
411 if self.handlers.pointer_inputs().is_empty() || self.surface.is_some() {
412 return None;
413 }
414 if let Some(clip_bounds) = self.hit_clip_bounds
415 && !clip_bounds.contains(x, y)
416 {
417 return None;
418 }
419 let grow_x = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.width) / 2.0).max(0.0);
420 let grow_y = ((MINIMUM_TOUCH_TARGET_SIZE - self.rect.height) / 2.0).max(0.0);
421 if grow_x <= 0.0 && grow_y <= 0.0 {
422 return None;
423 }
424 let right = self.rect.x + self.rect.width;
425 let bottom = self.rect.y + self.rect.height;
426 let in_reach = x >= self.rect.x - grow_x
427 && x <= right + grow_x
428 && y >= self.rect.y - grow_y
429 && y <= bottom + grow_y;
430 if !in_reach {
431 return None;
432 }
433 let dx = (self.rect.x - x).max(x - right).max(0.0);
434 let dy = (self.rect.y - y).max(y - bottom).max(0.0);
435 Some(dx * dx + dy * dy)
436 }
437
438 fn localize_event(&self, event: &PointerEvent) -> PointerEvent {
439 let local = match &self.surface {
440 Some(surface) => surface
441 .drag_point(event)
442 .unwrap_or_else(|| self.world_to_local.map_point(event.global_position)),
443 None => self.world_to_local.map_point(event.global_position),
444 };
445 event.copy_with_local_position(Point {
446 x: local.x - self.local_bounds.x,
447 y: local.y - self.local_bounds.y,
448 })
449 }
450
451 fn dispatch_modifier_slices(&self, modifier_slices: &ModifierNodeSlices, event: PointerEvent) {
452 if should_skip_consumed_event(&event) {
453 return;
454 }
455
456 modifier_slices.dispatch_pointer_event(self.localize_event(&event));
457 }
458
459 fn live_modifier_slices(&self, applier: &mut MemoryApplier) -> Option<Rc<ModifierNodeSlices>> {
460 if let Ok(modifier_slices) =
461 applier.with_node::<LayoutNode, _>(self.node_id, |node| node.modifier_slices_snapshot())
462 {
463 return Some(modifier_slices);
464 }
465
466 applier
467 .with_node::<SubcomposeLayoutNode, _>(self.node_id, |node| {
468 node.modifier_slices_snapshot()
469 })
470 .ok()
471 }
472}
473
474fn is_terminal_pointer_event(kind: PointerEventKind) -> bool {
475 matches!(kind, PointerEventKind::Up | PointerEventKind::Cancel)
476}
477
478fn should_skip_consumed_event(event: &PointerEvent) -> bool {
479 event.is_consumed() && !is_terminal_pointer_event(event.kind)
480}
481
482impl HitTestTarget for HitRegion {
483 fn node_id(&self) -> NodeId {
484 self.node_id
485 }
486
487 fn pointer_icon(&self) -> Option<PointerIcon> {
488 self.handlers.pointer_icon().cloned()
489 }
490
491 fn capture_path(&self) -> Vec<NodeId> {
492 self.capture_path.clone()
493 }
494
495 fn dispatch(&self, event: PointerEvent) {
496 self.dispatch_modifier_slices(&self.handlers, event);
497 }
498
499 fn dispatch_with_applier(&self, applier: &mut MemoryApplier, event: PointerEvent) {
500 if let Some(modifier_slices) = self.live_modifier_slices(applier) {
501 self.dispatch_modifier_slices(modifier_slices.as_ref(), event);
502 return;
503 }
504
505 self.diagnostics.record_live_modifier_slice_lookup_miss();
506 self.dispatch_modifier_slices(&self.handlers, event);
507 }
508}
509
510#[derive(Default)]
511struct HitBuffers {
512 hit_clips: Vec<HitClip>,
513 capture_path: Vec<NodeId>,
514}
515
516#[derive(Default)]
519struct HitList {
520 hits: Vec<HitRegion>,
521 buffers: Vec<HitBuffers>,
522 next_z: usize,
524 node_index: HashMap<NodeId, usize>,
526 stale: bool,
529}
530
531impl HitList {
532 fn clear(&mut self) {
534 self.buffers.clear();
535 for hit in self.hits.drain(..) {
536 let mut buffers = HitBuffers {
537 hit_clips: hit.hit_clips,
538 capture_path: hit.capture_path,
539 };
540 buffers.hit_clips.clear();
541 buffers.capture_path.clear();
542 self.buffers.push(buffers);
543 }
544 self.node_index.clear();
545 self.next_z = 0;
546 }
547}
548
549pub(crate) struct HitTargets<'a> {
552 list: &'a mut HitList,
553 diagnostics: &'a Rc<RenderDiagnostics>,
554}
555
556impl HitTargets<'_> {
557 pub(crate) fn push(
559 &mut self,
560 node_id: NodeId,
561 capture_path: &[NodeId],
562 geometry: HitGeometry<'_>,
563 target: HitTargetSpec<'_>,
564 ) {
565 let HitTargetSpec { shape, handlers } = target;
566 if handlers.pointer_inputs().is_empty() && handlers.pointer_icon().is_none() {
567 return;
568 }
569 let list = &mut *self.list;
570 let mut buffers = list.buffers.pop().unwrap_or_default();
571 buffers.capture_path.extend_from_slice(capture_path);
572
573 let z_index = list.next_z;
574 list.next_z += 1;
575 let hit_index = list.hits.len();
576 list.hits.push(HitRegion::with_diagnostics(HitRegionInit {
577 node_id,
578 capture_path: buffers.capture_path,
579 geometry,
580 clip_buffer: buffers.hit_clips,
581 shape,
582 handlers: Rc::clone(handlers),
583 z_index,
584 diagnostics: Rc::clone(self.diagnostics),
585 }));
586 list.node_index.insert(node_id, hit_index);
587 }
588}
589
590pub struct Scene {
592 pub graph: Option<RenderGraph>,
594 hits: RefCell<HitList>,
598 diagnostics: Rc<RenderDiagnostics>,
599 layer_moves: u64,
602}
603
604#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
607pub struct HitStorage {
608 pub hits_len: usize,
610 pub hits_capacity: usize,
612 pub node_index_len: usize,
614 pub node_index_capacity: usize,
616}
617
618impl Scene {
619 pub fn new() -> Self {
621 Self {
622 graph: None,
623 hits: RefCell::default(),
624 diagnostics: Rc::new(RenderDiagnostics::new()),
625 layer_moves: 0,
626 }
627 }
628
629 pub fn diagnostics(&self) -> &RenderDiagnostics {
631 self.diagnostics.as_ref()
632 }
633
634 pub fn rebuild_from_applier(&mut self, applier: &MemoryApplier, root: NodeId) {
637 let previous = self.graph.take();
638 self.hits.get_mut().stale = true;
639 self.layer_moves += 1;
640 let Some(graph) =
641 crate::scene_builder::rebuild_graph_from_applier(applier, root, 1.0, previous)
642 else {
643 return;
644 };
645 crate::scene_builder::publish_space(applier, &graph);
646 self.graph = Some(graph);
647 }
648
649 fn fresh_hits(&self) -> Ref<'_, HitList> {
652 if self.hits.borrow().stale {
653 let mut list = self.hits.borrow_mut();
654 list.clear();
655 list.stale = false;
656 if let Some(graph) = &self.graph {
657 crate::hit_graph::collect_hits_from_graph(
658 &graph.root,
659 ProjectiveTransform::identity(),
660 &mut HitTargets {
661 list: &mut list,
662 diagnostics: &self.diagnostics,
663 },
664 None,
665 );
666 }
667 }
668 self.hits.borrow()
669 }
670
671 pub fn hit_targets(&self) -> Vec<HitRegion> {
673 self.fresh_hits().hits.clone()
674 }
675
676 pub fn hit_storage(&self) -> HitStorage {
678 let list = self.hits.borrow();
679 HitStorage {
680 hits_len: list.hits.len(),
681 hits_capacity: list.hits.capacity(),
682 node_index_len: list.node_index.len(),
683 node_index_capacity: list.node_index.capacity(),
684 }
685 }
686
687 pub fn update_from_applier(
693 &mut self,
694 applier: &MemoryApplier,
695 root: NodeId,
696 updates: SceneUpdates<'_>,
697 ) {
698 let report = if updates.is_empty() {
699 None
700 } else {
701 self.graph.as_mut().map(|graph| {
702 crate::scene_builder::update_graph_from_applier_report(applier, graph, updates, 1.0)
703 })
704 };
705 let Some(report) = report.filter(|report| report.applied()) else {
706 self.rebuild_from_applier(applier, root);
707 return;
708 };
709 if let Some(graph) = &self.graph {
710 crate::scene_builder::publish_space(applier, graph);
711 }
712 if report.hit_graph_dirty {
713 self.hits.get_mut().stale = true;
714 }
715 self.layer_moves += u64::from(report.layers_moved);
716 }
717
718 pub(crate) fn push_hit(
721 &mut self,
722 node_id: NodeId,
723 capture_path: &[NodeId],
724 geometry: HitGeometry<'_>,
725 target: HitTargetSpec<'_>,
726 ) {
727 HitTargets {
728 list: self.hits.get_mut(),
729 diagnostics: &self.diagnostics,
730 }
731 .push(node_id, capture_path, geometry, target);
732 }
733
734 fn clear_hits(&mut self) {
737 let list = self.hits.get_mut();
738 list.clear();
739 list.stale = false;
740 }
741
742 pub fn replace_graph(&mut self, graph: RenderGraph) {
744 self.graph = Some(graph);
745 }
746}
747
748impl Default for Scene {
749 fn default() -> Self {
750 Self::new()
751 }
752}
753
754impl RenderScene for Scene {
755 type HitTarget = HitRegion;
756
757 fn clear(&mut self) {
758 self.graph = None;
759 self.clear_hits();
760 }
761
762 fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
763 let list = self.fresh_hits();
764 let mut hit_indices: Vec<usize> = list
765 .hits
766 .iter()
767 .enumerate()
768 .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
769 .collect();
770
771 hit_indices.sort_by_key(|&index| Reverse(list.hits[index].z_index));
772 hit_indices
773 .into_iter()
774 .map(|index| list.hits[index].clone())
775 .collect()
776 }
777
778 fn hit_test_near(&self, x: f32, y: f32) -> Option<Self::HitTarget> {
779 self.fresh_hits()
780 .hits
781 .iter()
782 .filter_map(|hit| hit.reach_distance(x, y).map(|distance| (distance, hit)))
783 .min_by(|(near, hit), (other_near, other)| {
784 near.total_cmp(other_near)
785 .then_with(|| other.z_index.cmp(&hit.z_index))
786 })
787 .map(|(_, hit)| hit.clone())
788 }
789
790 fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
791 let list = self.fresh_hits();
792 list.node_index
793 .get(&node_id)
794 .and_then(|&index| list.hits.get(index))
795 .cloned()
796 }
797
798 fn hit_test_ray(&self, scene: NodeId, ray: Ray) -> Vec<Self::HitTarget> {
799 let list = self.fresh_hits();
800 let mut hit: Vec<&HitRegion> = list
801 .hits
802 .iter()
803 .filter(|hit| {
804 hit.surface.as_ref().is_some_and(|surface| {
805 surface.target.pick.scene() == Some(scene)
806 && surface
807 .local_point_along(ray)
808 .is_some_and(|local| hit.contains_local(local))
809 })
810 })
811 .collect();
812 hit.sort_by_key(|hit| Reverse(hit.z_index));
813 hit.into_iter().cloned().collect()
814 }
815
816 fn surface_windows(&self, visit: &mut dyn FnMut(NodeId, ProjectiveTransform)) {
817 let Some(graph) = &self.graph else {
818 return;
819 };
820 let root = ProjectiveTransform::identity();
821 crate::space::for_each_window_eye(&graph.root, root, &mut |_, view, to_window| {
822 for seen in &view.surfaces {
823 if let (Some(node_id), Some(content)) =
824 (seen.layer.node_id, view.content_to_window(seen, to_window))
825 {
826 visit(node_id, content);
827 }
828 }
829 });
830 }
831
832 fn surface_window_bounds(&self, node_id: NodeId) -> Option<Rect> {
833 let list = self.fresh_hits();
834 let hit = list.hits.get(*list.node_index.get(&node_id)?)?;
835 let surface = hit.surface.as_ref()?;
836 surface
837 .target
838 .window_bounds(&surface.content_to_local, hit.local_bounds)
839 }
840
841 fn node_window_bounds(&self, visit: &mut dyn FnMut(NodeId, Rect)) {
842 let Some(graph) = &self.graph else {
843 return;
844 };
845 let mut surfaces = HashMap::default();
846 self.surface_windows(&mut |node_id, content| {
847 surfaces.insert(node_id, content);
848 });
849 crate::node_window_bounds::for_each_node_window_bounds(
850 &graph.root,
851 ProjectiveTransform::identity(),
852 &surfaces,
853 visit,
854 );
855 }
856
857 fn layer_moves(&self) -> u64 {
858 self.layer_moves
859 }
860
861 fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) -> bool {
862 if let Some(graph) = &self.graph {
863 graph.collect_retained_visual_observation_nodes(nodes);
864 } else {
865 nodes.clear();
866 }
867 true
868 }
869}
870
871fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
872 if !rect.contains(point.x, point.y) {
873 return false;
874 }
875
876 let local_x = point.x - rect.x;
877 let local_y = point.y - rect.y;
878 let radii = shape.resolve(rect.width, rect.height);
879 let tl = radii.top_left;
880 let tr = radii.top_right;
881 let bl = radii.bottom_left;
882 let br = radii.bottom_right;
883
884 if local_x < tl && local_y < tl {
885 let dx = tl - local_x;
886 let dy = tl - local_y;
887 return dx * dx + dy * dy <= tl * tl;
888 }
889
890 if local_x > rect.width - tr && local_y < tr {
891 let dx = local_x - (rect.width - tr);
892 let dy = tr - local_y;
893 return dx * dx + dy * dy <= tr * tr;
894 }
895
896 if local_x < bl && local_y > rect.height - bl {
897 let dx = bl - local_x;
898 let dy = local_y - (rect.height - bl);
899 return dx * dx + dy * dy <= bl * bl;
900 }
901
902 if local_x > rect.width - br && local_y > rect.height - br {
903 let dx = local_x - (rect.width - br);
904 let dy = local_y - (rect.height - br);
905 return dx * dx + dy * dy <= br * br;
906 }
907
908 true
909}
910
911fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
912 point_in_triangle(point, quad[0], quad[1], quad[3])
913 || point_in_triangle(point, quad[0], quad[3], quad[2])
914}
915
916fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
917 let d1 = triangle_sign(point, a, b);
918 let d2 = triangle_sign(point, b, c);
919 let d3 = triangle_sign(point, c, a);
920 let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
921 let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
922 !(has_negative && has_positive)
923}
924
925fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
926 (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
927}
928
929#[cfg(test)]
930#[path = "tests/graph_scene_tests.rs"]
931mod tests;