1use std::{
2 cell::{Cell, 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: 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
516pub(crate) struct HitTargets<'a> {
519 hits: &'a mut Vec<HitRegion>,
520 buffers: &'a mut Vec<HitBuffers>,
521 next_z: &'a mut usize,
522 node_index: &'a mut HashMap<NodeId, usize>,
523 diagnostics: &'a Rc<RenderDiagnostics>,
524}
525
526impl HitTargets<'_> {
527 pub(crate) fn push(
529 &mut self,
530 node_id: NodeId,
531 capture_path: &[NodeId],
532 geometry: HitGeometry<'_>,
533 target: HitTargetSpec<'_>,
534 ) {
535 let HitTargetSpec { shape, handlers } = target;
536 if handlers.pointer_inputs().is_empty() && handlers.pointer_icon().is_none() {
537 return;
538 }
539 let mut buffers = self.buffers.pop().unwrap_or_default();
540 buffers.capture_path.extend_from_slice(capture_path);
541
542 let z_index = *self.next_z;
543 *self.next_z += 1;
544 let hit_index = self.hits.len();
545 self.hits.push(HitRegion::with_diagnostics(HitRegionInit {
546 node_id,
547 capture_path: buffers.capture_path,
548 geometry,
549 clip_buffer: buffers.hit_clips,
550 shape,
551 handlers: Rc::clone(handlers),
552 z_index,
553 diagnostics: Rc::clone(self.diagnostics),
554 }));
555 self.node_index.insert(node_id, hit_index);
556 }
557}
558
559pub struct Scene {
561 pub graph: Option<RenderGraph>,
563 pub hits: Vec<HitRegion>,
565 hit_buffers: Vec<HitBuffers>,
566 pub next_hit_z: usize,
568 pub node_index: HashMap<NodeId, usize>,
570 diagnostics: Rc<RenderDiagnostics>,
571}
572
573impl Scene {
574 pub fn new() -> Self {
576 Self {
577 graph: None,
578 hits: Vec::new(),
579 hit_buffers: Vec::new(),
580 next_hit_z: 0,
581 node_index: HashMap::default(),
582 diagnostics: Rc::new(RenderDiagnostics::new()),
583 }
584 }
585
586 pub fn diagnostics(&self) -> &RenderDiagnostics {
588 self.diagnostics.as_ref()
589 }
590
591 pub fn rebuild_from_applier(&mut self, applier: &MemoryApplier, root: NodeId) {
594 let previous = self.graph.take();
595 self.clear_hits();
596 let Some(graph) =
597 crate::scene_builder::rebuild_graph_from_applier(applier, root, 1.0, previous)
598 else {
599 return;
600 };
601 crate::scene_builder::publish_space(applier, &graph);
602 self.graph = Some(graph);
603 self.collect_hits();
604 }
605
606 fn collect_hits(&mut self) {
608 let Self {
609 graph: Some(graph),
610 hits,
611 hit_buffers,
612 next_hit_z,
613 node_index,
614 diagnostics,
615 } = self
616 else {
617 return;
618 };
619 let mut targets = HitTargets {
620 hits,
621 buffers: hit_buffers,
622 next_z: next_hit_z,
623 node_index,
624 diagnostics,
625 };
626 crate::hit_graph::collect_hits_from_graph(
627 &graph.root,
628 ProjectiveTransform::identity(),
629 &mut targets,
630 None,
631 );
632 }
633
634 pub fn update_from_applier(
639 &mut self,
640 applier: &MemoryApplier,
641 root: NodeId,
642 updates: SceneUpdates<'_>,
643 refresh_hits: bool,
644 ) {
645 let report = if updates.is_empty() {
646 None
647 } else {
648 self.graph.as_mut().map(|graph| {
649 crate::scene_builder::update_graph_from_applier_report(applier, graph, updates, 1.0)
650 })
651 };
652 let Some(report) = report.filter(|report| report.applied()) else {
653 self.rebuild_from_applier(applier, root);
654 return;
655 };
656 if let Some(graph) = &self.graph {
657 crate::scene_builder::publish_space(applier, graph);
658 }
659 if !refresh_hits && !report.hit_graph_dirty {
660 return;
661 }
662 self.clear_hits();
663 self.collect_hits();
664 }
665
666 pub fn push_hit(
669 &mut self,
670 node_id: NodeId,
671 capture_path: &[NodeId],
672 geometry: HitGeometry<'_>,
673 target: HitTargetSpec<'_>,
674 ) {
675 HitTargets {
676 hits: &mut self.hits,
677 buffers: &mut self.hit_buffers,
678 next_z: &mut self.next_hit_z,
679 node_index: &mut self.node_index,
680 diagnostics: &self.diagnostics,
681 }
682 .push(node_id, capture_path, geometry, target);
683 }
684
685 pub fn clear_hits(&mut self) {
687 self.hit_buffers.clear();
688 for hit in self.hits.drain(..) {
689 let mut buffers = HitBuffers {
690 hit_clips: hit.hit_clips,
691 capture_path: hit.capture_path,
692 };
693 buffers.hit_clips.clear();
694 buffers.capture_path.clear();
695 self.hit_buffers.push(buffers);
696 }
697 self.node_index.clear();
698 self.next_hit_z = 0;
699 }
700
701 pub fn replace_graph(&mut self, graph: RenderGraph) {
703 self.graph = Some(graph);
704 }
705}
706
707impl Default for Scene {
708 fn default() -> Self {
709 Self::new()
710 }
711}
712
713impl RenderScene for Scene {
714 type HitTarget = HitRegion;
715
716 fn clear(&mut self) {
717 self.graph = None;
718 self.clear_hits();
719 }
720
721 fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
722 let mut hit_indices: Vec<usize> = self
723 .hits
724 .iter()
725 .enumerate()
726 .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
727 .collect();
728
729 hit_indices.sort_by_key(|&index| Reverse(self.hits[index].z_index));
730 hit_indices
731 .into_iter()
732 .map(|index| self.hits[index].clone())
733 .collect()
734 }
735
736 fn hit_test_near(&self, x: f32, y: f32) -> Option<Self::HitTarget> {
737 self.hits
738 .iter()
739 .filter_map(|hit| hit.reach_distance(x, y).map(|distance| (distance, hit)))
740 .min_by(|(near, hit), (other_near, other)| {
741 near.total_cmp(other_near)
742 .then_with(|| other.z_index.cmp(&hit.z_index))
743 })
744 .map(|(_, hit)| hit.clone())
745 }
746
747 fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
748 self.node_index
749 .get(&node_id)
750 .and_then(|&index| self.hits.get(index))
751 .cloned()
752 }
753
754 fn hit_test_ray(&self, scene: NodeId, ray: Ray) -> Vec<Self::HitTarget> {
755 let mut hit: Vec<&HitRegion> = self
756 .hits
757 .iter()
758 .filter(|hit| {
759 hit.surface.as_ref().is_some_and(|surface| {
760 surface.target.pick.scene() == Some(scene)
761 && surface
762 .local_point_along(ray)
763 .is_some_and(|local| hit.contains_local(local))
764 })
765 })
766 .collect();
767 hit.sort_by_key(|hit| Reverse(hit.z_index));
768 hit.into_iter().cloned().collect()
769 }
770
771 fn surface_windows(&self, visit: &mut dyn FnMut(NodeId, ProjectiveTransform)) {
772 let Some(graph) = &self.graph else {
773 return;
774 };
775 let root = ProjectiveTransform::identity();
776 crate::space::for_each_window_eye(&graph.root, root, &mut |_, view, to_window| {
777 for seen in &view.surfaces {
778 if let (Some(node_id), Some(content)) =
779 (seen.layer.node_id, view.content_to_window(seen, to_window))
780 {
781 visit(node_id, content);
782 }
783 }
784 });
785 }
786
787 fn surface_window_bounds(&self, node_id: NodeId) -> Option<Rect> {
788 let hit = self.hits.get(*self.node_index.get(&node_id)?)?;
789 let surface = hit.surface.as_ref()?;
790 surface
791 .target
792 .window_bounds(&surface.content_to_local, hit.local_bounds)
793 }
794
795 fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) -> bool {
796 if let Some(graph) = &self.graph {
797 graph.collect_retained_visual_observation_nodes(nodes);
798 } else {
799 nodes.clear();
800 }
801 true
802 }
803}
804
805fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
806 if !rect.contains(point.x, point.y) {
807 return false;
808 }
809
810 let local_x = point.x - rect.x;
811 let local_y = point.y - rect.y;
812 let radii = shape.resolve(rect.width, rect.height);
813 let tl = radii.top_left;
814 let tr = radii.top_right;
815 let bl = radii.bottom_left;
816 let br = radii.bottom_right;
817
818 if local_x < tl && local_y < tl {
819 let dx = tl - local_x;
820 let dy = tl - local_y;
821 return dx * dx + dy * dy <= tl * tl;
822 }
823
824 if local_x > rect.width - tr && local_y < tr {
825 let dx = local_x - (rect.width - tr);
826 let dy = tr - local_y;
827 return dx * dx + dy * dy <= tr * tr;
828 }
829
830 if local_x < bl && local_y > rect.height - bl {
831 let dx = bl - local_x;
832 let dy = local_y - (rect.height - bl);
833 return dx * dx + dy * dy <= bl * bl;
834 }
835
836 if local_x > rect.width - br && local_y > rect.height - br {
837 let dx = local_x - (rect.width - br);
838 let dy = local_y - (rect.height - br);
839 return dx * dx + dy * dy <= br * br;
840 }
841
842 true
843}
844
845fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
846 point_in_triangle(point, quad[0], quad[1], quad[3])
847 || point_in_triangle(point, quad[0], quad[3], quad[2])
848}
849
850fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
851 let d1 = triangle_sign(point, a, b);
852 let d2 = triangle_sign(point, b, c);
853 let d3 = triangle_sign(point, c, a);
854 let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
855 let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
856 !(has_negative && has_positive)
857}
858
859fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
860 (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
861}
862
863#[cfg(test)]
864#[path = "tests/graph_scene_tests.rs"]
865mod tests;