Skip to main content

open_gpui_canvas/
geometry_facts.rs

1use crate::{
2    CanvasConnectionEndpointRole, CanvasDefaultEdgeRouter, CanvasDocument, CanvasEdge,
3    CanvasEdgeRouter, CanvasEndpoint, CanvasHandle, CanvasKindRegistry, CanvasNode, CanvasRecordId,
4    CanvasRoutePath, CanvasRouteRequest, CanvasRouteSegment, CanvasSelection, CanvasShape,
5    DocumentError, HitOptions, HitRecord, HitTarget,
6};
7use open_gpui::{Bounds, Pixels, Point, px};
8
9#[derive(Clone, Debug, PartialEq)]
10pub struct CanvasResolvedEdgeGeometry {
11    pub path: CanvasRoutePath,
12    pub bounds: Bounds<Pixels>,
13    pub hit_radius: Pixels,
14}
15
16impl CanvasResolvedEdgeGeometry {
17    pub fn contains_point(&self, point: Point<Pixels>, margin: Pixels) -> bool {
18        let radius = (self.hit_radius + margin).as_f32().max(0.0);
19        let Some(distance_squared) = self.distance_to_point_squared(point) else {
20            return false;
21        };
22        distance_squared <= radius * radius
23    }
24
25    pub fn distance_to_point_squared(&self, point: Point<Pixels>) -> Option<f32> {
26        self.nearest_point_to_route(point)
27            .map(|nearest| nearest.distance_squared)
28    }
29
30    pub fn nearest_point(&self, point: Point<Pixels>) -> Option<Point<Pixels>> {
31        self.nearest_point_to_route(point)
32            .map(|nearest| nearest.point)
33    }
34
35    fn nearest_point_to_route(&self, point: Point<Pixels>) -> Option<NearestPoint> {
36        self.path
37            .segments
38            .iter()
39            .filter_map(|segment| segment_nearest_point(segment, point))
40            .min_by(|left, right| left.distance_squared.total_cmp(&right.distance_squared))
41    }
42}
43
44#[derive(Clone, Copy, Debug)]
45pub struct CanvasGeometryFacts<'a, R = CanvasDefaultEdgeRouter> {
46    document: &'a CanvasDocument,
47    router: R,
48    kind_registry: Option<&'a CanvasKindRegistry>,
49}
50
51impl<'a> CanvasGeometryFacts<'a> {
52    pub fn new(document: &'a CanvasDocument) -> Self {
53        Self::with_router(document, CanvasDefaultEdgeRouter)
54    }
55
56    pub fn with_kind_registry(
57        document: &'a CanvasDocument,
58        kind_registry: &'a CanvasKindRegistry,
59    ) -> Self {
60        Self::with_router_and_kind_registry(document, CanvasDefaultEdgeRouter, Some(kind_registry))
61    }
62}
63
64impl<'a, R> CanvasGeometryFacts<'a, R>
65where
66    R: CanvasEdgeRouter,
67{
68    pub fn with_router(document: &'a CanvasDocument, router: R) -> Self {
69        Self::with_router_and_kind_registry(document, router, None)
70    }
71
72    pub fn with_router_and_kind_registry(
73        document: &'a CanvasDocument,
74        router: R,
75        kind_registry: Option<&'a CanvasKindRegistry>,
76    ) -> Self {
77        Self {
78            document,
79            router,
80            kind_registry,
81        }
82    }
83
84    pub fn document(&self) -> &'a CanvasDocument {
85        self.document
86    }
87
88    pub fn kind_registry(&self) -> Option<&'a CanvasKindRegistry> {
89        self.kind_registry
90    }
91
92    pub fn node_bounds(&self, node: &CanvasNode) -> Bounds<Pixels> {
93        self.kind_registry
94            .and_then(|registry| registry.node_bounds(node))
95            .unwrap_or_else(|| node.bounds())
96    }
97
98    pub fn shape_bounds(&self, shape: &CanvasShape) -> Bounds<Pixels> {
99        self.kind_registry
100            .and_then(|registry| registry.shape_bounds(shape))
101            .unwrap_or(shape.bounds)
102    }
103
104    pub fn handle_bounds(&self, node: &CanvasNode, handle: &CanvasHandle) -> Bounds<Pixels> {
105        Bounds::centered_at(self.resolved_handle_position(node, handle), handle.size)
106    }
107
108    pub fn endpoint_position(
109        &self,
110        endpoint: &CanvasEndpoint,
111    ) -> Result<Point<Pixels>, DocumentError> {
112        let node = self
113            .document
114            .node(&endpoint.node_id)
115            .ok_or_else(|| DocumentError::MissingNode(endpoint.node_id.clone()))?;
116
117        if let Some(handle_id) = &endpoint.handle_id {
118            let handle =
119                node.handle(Some(handle_id))
120                    .ok_or_else(|| DocumentError::MissingHandle {
121                        node_id: endpoint.node_id.clone(),
122                        handle_id: handle_id.clone(),
123                    })?;
124            return Ok(self.resolved_handle_position(node, handle));
125        }
126
127        Ok(self.node_bounds(node).center())
128    }
129
130    pub fn edge_route_path(&self, edge: &CanvasEdge) -> Result<CanvasRoutePath, DocumentError> {
131        let source = self.endpoint_position(&edge.source)?;
132        let target = self.endpoint_position(&edge.target)?;
133        Ok(self.router.route_edge(CanvasRouteRequest {
134            edge,
135            source,
136            target,
137        }))
138    }
139
140    pub fn edge_geometry(
141        &self,
142        edge: &CanvasEdge,
143    ) -> Result<CanvasResolvedEdgeGeometry, DocumentError> {
144        let path = self.edge_route_path(edge)?;
145        let bounds = match path.bounds() {
146            Some(bounds) => bounds,
147            None => {
148                let source = self.endpoint_position(&edge.source)?;
149                let target = self.endpoint_position(&edge.target)?;
150                Bounds::from_corners(
151                    Point::new(source.x.min(target.x), source.y.min(target.y)),
152                    Point::new(source.x.max(target.x), source.y.max(target.y)),
153                )
154            }
155        };
156
157        let hit_radius = edge_interaction_radius(edge);
158        Ok(CanvasResolvedEdgeGeometry {
159            path,
160            bounds: bounds.dilate(hit_radius),
161            hit_radius,
162        })
163    }
164
165    pub fn edge_bounds(&self, edge: &CanvasEdge) -> Result<Bounds<Pixels>, DocumentError> {
166        Ok(self.edge_geometry(edge)?.bounds)
167    }
168
169    pub fn record_geometry(&self, record_id: &CanvasRecordId) -> Option<CanvasRecordGeometry> {
170        match record_id {
171            CanvasRecordId::Node(id) => {
172                let node = self.document.node(id)?;
173                Some(CanvasRecordGeometry {
174                    id: CanvasRecordId::Node(node.id.clone()),
175                    bounds: self.node_bounds(node),
176                    z_index: node.z_index,
177                    hidden: node.hidden,
178                    locked: node.locked,
179                })
180            }
181            CanvasRecordId::Edge(id) => {
182                let edge = self.document.edge(id)?;
183                let bounds = self.edge_bounds(edge).ok()?;
184                Some(CanvasRecordGeometry {
185                    id: CanvasRecordId::Edge(edge.id.clone()),
186                    bounds,
187                    z_index: edge.z_index,
188                    hidden: edge.hidden,
189                    locked: edge.locked,
190                })
191            }
192            CanvasRecordId::Shape(id) => {
193                let shape = self.document.shape(id)?;
194                Some(CanvasRecordGeometry {
195                    id: CanvasRecordId::Shape(shape.id.clone()),
196                    bounds: self.shape_bounds(shape),
197                    z_index: shape.z_index,
198                    hidden: shape.hidden,
199                    locked: shape.locked,
200                })
201            }
202        }
203    }
204
205    pub fn record_geometries(&self) -> Vec<CanvasRecordGeometry> {
206        self.document
207            .nodes()
208            .filter_map(|node| self.record_geometry(&CanvasRecordId::Node(node.id.clone())))
209            .chain(
210                self.document.shapes().filter_map(|shape| {
211                    self.record_geometry(&CanvasRecordId::Shape(shape.id.clone()))
212                }),
213            )
214            .chain(
215                self.document.edges().filter_map(|edge| {
216                    self.record_geometry(&CanvasRecordId::Edge(edge.id.clone()))
217                }),
218            )
219            .collect()
220    }
221
222    pub fn selected_record_geometries(
223        &self,
224        selection: &CanvasSelection,
225    ) -> Vec<CanvasRecordGeometry> {
226        selection
227            .selected_nodes()
228            .filter_map(|id| self.record_geometry(&CanvasRecordId::Node(id.clone())))
229            .chain(
230                selection
231                    .selected_shapes()
232                    .filter_map(|id| self.record_geometry(&CanvasRecordId::Shape(id.clone()))),
233            )
234            .collect()
235    }
236
237    pub fn selected_bounds(&self, selection: &CanvasSelection) -> Option<Bounds<Pixels>> {
238        union_record_geometry_bounds(
239            self.selected_record_geometries(selection)
240                .into_iter()
241                .filter(CanvasRecordGeometry::is_visible_unlocked),
242        )
243    }
244
245    pub fn node_shape_bounds_for_records<'b>(
246        &self,
247        record_ids: impl IntoIterator<Item = &'b CanvasRecordId>,
248    ) -> Option<Bounds<Pixels>> {
249        union_record_geometry_bounds(record_ids.into_iter().filter_map(|record_id| {
250            if !matches!(
251                record_id,
252                CanvasRecordId::Node(_) | CanvasRecordId::Shape(_)
253            ) {
254                return None;
255            }
256
257            self.record_geometry(record_id)
258                .filter(CanvasRecordGeometry::is_visible_unlocked)
259        }))
260    }
261
262    pub(crate) fn hit_records(&self) -> Vec<HitRecord> {
263        let mut records = Vec::new();
264
265        for node in self.document.nodes() {
266            records.extend(self.hit_records_for_record(&CanvasRecordId::Node(node.id.clone())));
267        }
268
269        for shape in self.document.shapes() {
270            records.extend(self.hit_records_for_record(&CanvasRecordId::Shape(shape.id.clone())));
271        }
272
273        for edge in self.document.edges() {
274            records.extend(self.hit_records_for_record(&CanvasRecordId::Edge(edge.id.clone())));
275        }
276
277        records
278    }
279
280    pub(crate) fn hit_records_for_record(&self, record_id: &CanvasRecordId) -> Vec<HitRecord> {
281        let mut records = Vec::new();
282
283        match record_id {
284            CanvasRecordId::Node(id) => {
285                let Some(node) = self.document.node(id) else {
286                    return records;
287                };
288
289                records.push(HitRecord {
290                    target: HitTarget::Node(node.id.clone()),
291                    bounds: self.node_bounds(node),
292                    z_index: node.z_index,
293                    hidden: node.hidden,
294                    locked: node.locked,
295                });
296
297                for handle in &node.handles {
298                    records.push(HitRecord {
299                        target: HitTarget::Handle {
300                            node_id: node.id.clone(),
301                            handle_id: handle.id.clone(),
302                        },
303                        bounds: self.handle_bounds(node, handle),
304                        z_index: node.z_index,
305                        hidden: node.hidden || handle.hidden || !handle.connectable,
306                        locked: node.locked,
307                    });
308                }
309            }
310            CanvasRecordId::Edge(id) => {
311                let Some(edge) = self.document.edge(id) else {
312                    return records;
313                };
314
315                if let Ok(bounds) = self.edge_bounds(edge) {
316                    records.push(HitRecord {
317                        target: HitTarget::Edge(edge.id.clone()),
318                        bounds,
319                        z_index: edge.z_index,
320                        hidden: edge.hidden,
321                        locked: edge.locked,
322                    });
323                }
324            }
325            CanvasRecordId::Shape(id) => {
326                let Some(shape) = self.document.shape(id) else {
327                    return records;
328                };
329
330                records.push(HitRecord {
331                    target: HitTarget::Shape(shape.id.clone()),
332                    bounds: self.shape_bounds(shape),
333                    z_index: shape.z_index,
334                    hidden: shape.hidden,
335                    locked: shape.locked,
336                });
337            }
338        }
339
340        records
341    }
342
343    pub fn record_contains_point(
344        &self,
345        record: &HitRecord,
346        point: Point<Pixels>,
347        options: HitOptions,
348    ) -> bool {
349        self.record_contains_point_with_edge_geometry(record, point, options, None)
350    }
351
352    pub fn record_intersects_bounds(
353        &self,
354        record: &HitRecord,
355        bounds: Bounds<Pixels>,
356        options: HitOptions,
357    ) -> bool {
358        if !record_options_match(record, options) {
359            return false;
360        }
361
362        let record_bounds = if options.margin == Pixels::ZERO {
363            record.bounds
364        } else {
365            record.bounds.dilate(options.margin)
366        };
367        let Some(intersection) = intersect_bounds(record_bounds, bounds) else {
368            return false;
369        };
370
371        match &record.target {
372            HitTarget::Node(id) => {
373                if let Some(intersects) = self.document.node(id).and_then(|node| {
374                    self.kind_registry.and_then(|registry| {
375                        registry.node_intersects_bounds(node, record.bounds, bounds, options.margin)
376                    })
377                }) {
378                    return intersects;
379                }
380
381                selection_sample_points(intersection)
382                    .into_iter()
383                    .any(|point| self.record_contains_point(record, point, options))
384            }
385            HitTarget::Shape(id) => {
386                if let Some(intersects) = self.document.shape(id).and_then(|shape| {
387                    self.kind_registry.and_then(|registry| {
388                        registry.shape_intersects_bounds(
389                            shape,
390                            record.bounds,
391                            bounds,
392                            options.margin,
393                        )
394                    })
395                }) {
396                    return intersects;
397                }
398
399                selection_sample_points(intersection)
400                    .into_iter()
401                    .any(|point| self.record_contains_point(record, point, options))
402            }
403            HitTarget::Edge(_) | HitTarget::Handle { .. } => true,
404        }
405    }
406
407    pub(crate) fn record_contains_point_with_edge_geometry(
408        &self,
409        record: &HitRecord,
410        point: Point<Pixels>,
411        options: HitOptions,
412        edge_geometry: Option<&CanvasResolvedEdgeGeometry>,
413    ) -> bool {
414        if !record_options_match(record, options) {
415            return false;
416        }
417
418        let bounds = if options.margin == Pixels::ZERO {
419            record.bounds
420        } else {
421            record.bounds.dilate(options.margin)
422        };
423        if !bounds.contains(&point) {
424            return false;
425        }
426
427        match &record.target {
428            HitTarget::Node(id) => {
429                let Some(node) = self.document.node(id) else {
430                    return false;
431                };
432                self.kind_registry
433                    .and_then(|registry| {
434                        registry.node_contains_point(node, point, record.bounds, options.margin)
435                    })
436                    .unwrap_or(true)
437            }
438            HitTarget::Handle { .. } => true,
439            HitTarget::Shape(id) => {
440                let Some(shape) = self.document.shape(id) else {
441                    return false;
442                };
443                self.kind_registry
444                    .and_then(|registry| {
445                        registry.shape_contains_point(shape, point, record.bounds, options.margin)
446                    })
447                    .unwrap_or(true)
448            }
449            HitTarget::Edge(id) => {
450                let Some(edge) = self.document.edge(id) else {
451                    return false;
452                };
453                if let Some(edge_geometry) = edge_geometry {
454                    return edge_geometry.contains_point(point, options.margin);
455                }
456
457                self.edge_contains_point(edge, point, options.margin)
458                    .unwrap_or(false)
459            }
460        }
461    }
462
463    pub fn connection_endpoint_at<'h>(
464        &self,
465        records: impl IntoIterator<Item = &'h HitRecord>,
466        role: CanvasConnectionEndpointRole,
467    ) -> Option<CanvasEndpoint> {
468        for record in records {
469            match &record.target {
470                HitTarget::Handle { node_id, handle_id } => {
471                    let node = self.document.node(node_id)?;
472                    let handle = node.handle(Some(handle_id))?;
473                    return handle
474                        .is_pickable_connection_endpoint(role)
475                        .then(|| CanvasEndpoint {
476                            node_id: node_id.clone(),
477                            handle_id: Some(handle_id.clone()),
478                        });
479                }
480                HitTarget::Node(node_id) => {
481                    let node = self.document.node(node_id)?;
482                    if self.kind_registry.is_some_and(|registry| {
483                        registry.node_accepts_connection_endpoint(node, role)
484                    }) {
485                        return Some(CanvasEndpoint {
486                            node_id: node_id.clone(),
487                            handle_id: None,
488                        });
489                    }
490                }
491                HitTarget::Edge(_) | HitTarget::Shape(_) => {}
492            }
493        }
494
495        None
496    }
497
498    pub fn connection_preview_target<'h>(
499        &self,
500        records: impl IntoIterator<Item = &'h HitRecord>,
501        source: Point<Pixels>,
502        _current: Point<Pixels>,
503    ) -> Option<Point<Pixels>> {
504        let target = self.connection_endpoint_at(records, CanvasConnectionEndpointRole::Target)?;
505        let target_position = self.endpoint_position(&target).ok()?;
506        (target_position != source).then_some(target_position)
507    }
508
509    fn resolved_handle_position(&self, node: &CanvasNode, handle: &CanvasHandle) -> Point<Pixels> {
510        self.kind_registry
511            .and_then(|registry| registry.handle_position(node, &handle.id))
512            .unwrap_or(node.position + handle.position)
513    }
514
515    fn edge_contains_point(
516        &self,
517        edge: &CanvasEdge,
518        point: Point<Pixels>,
519        margin: Pixels,
520    ) -> Result<bool, DocumentError> {
521        let geometry = self.edge_geometry(edge)?;
522        Ok(geometry.contains_point(point, margin))
523    }
524}
525
526#[derive(Clone, Debug, PartialEq)]
527pub struct CanvasRecordGeometry {
528    pub id: CanvasRecordId,
529    pub bounds: Bounds<Pixels>,
530    pub z_index: i32,
531    pub hidden: bool,
532    pub locked: bool,
533}
534
535impl CanvasRecordGeometry {
536    pub fn is_node_or_shape(&self) -> bool {
537        matches!(self.id, CanvasRecordId::Node(_) | CanvasRecordId::Shape(_))
538    }
539
540    pub fn is_visible_unlocked(&self) -> bool {
541        !self.hidden && !self.locked
542    }
543}
544
545pub(crate) fn union_record_geometry_bounds(
546    geometries: impl IntoIterator<Item = CanvasRecordGeometry>,
547) -> Option<Bounds<Pixels>> {
548    geometries.into_iter().fold(None, |current, geometry| {
549        union_bounds(current, geometry.bounds)
550    })
551}
552
553fn union_bounds(current: Option<Bounds<Pixels>>, next: Bounds<Pixels>) -> Option<Bounds<Pixels>> {
554    Some(match current {
555        None => next,
556        Some(current) => Bounds::from_corners(
557            Point::new(
558                current.origin.x.min(next.origin.x),
559                current.origin.y.min(next.origin.y),
560            ),
561            Point::new(
562                (current.origin.x + current.size.width).max(next.origin.x + next.size.width),
563                (current.origin.y + current.size.height).max(next.origin.y + next.size.height),
564            ),
565        ),
566    })
567}
568
569fn intersect_bounds(left: Bounds<Pixels>, right: Bounds<Pixels>) -> Option<Bounds<Pixels>> {
570    let left_min_x = left.origin.x;
571    let left_min_y = left.origin.y;
572    let left_max_x = left.origin.x + left.size.width;
573    let left_max_y = left.origin.y + left.size.height;
574    let right_min_x = right.origin.x;
575    let right_min_y = right.origin.y;
576    let right_max_x = right.origin.x + right.size.width;
577    let right_max_y = right.origin.y + right.size.height;
578
579    let min_x = left_min_x.max(right_min_x);
580    let min_y = left_min_y.max(right_min_y);
581    let max_x = left_max_x.min(right_max_x);
582    let max_y = left_max_y.min(right_max_y);
583    if max_x < min_x || max_y < min_y {
584        return None;
585    }
586
587    Some(Bounds::from_corners(
588        Point::new(min_x, min_y),
589        Point::new(max_x, max_y),
590    ))
591}
592
593fn selection_sample_points(bounds: Bounds<Pixels>) -> [Point<Pixels>; 9] {
594    let left = bounds.origin.x;
595    let top = bounds.origin.y;
596    let right = bounds.origin.x + bounds.size.width;
597    let bottom = bounds.origin.y + bounds.size.height;
598    let center = bounds.center();
599
600    [
601        Point::new(left, top),
602        Point::new(center.x, top),
603        Point::new(right, top),
604        Point::new(left, center.y),
605        center,
606        Point::new(right, center.y),
607        Point::new(left, bottom),
608        Point::new(center.x, bottom),
609        Point::new(right, bottom),
610    ]
611}
612
613fn edge_interaction_radius(edge: &CanvasEdge) -> Pixels {
614    let stroke_width =
615        if edge.style.stroke_width.as_f32().is_finite() && edge.style.stroke_width > Pixels::ZERO {
616            edge.style.stroke_width
617        } else {
618            Pixels::ZERO
619        };
620    let interaction_width = if edge.route.interaction_width > stroke_width {
621        edge.route.interaction_width
622    } else {
623        stroke_width
624    };
625
626    interaction_width * 0.5
627}
628
629#[derive(Clone, Copy, Debug, PartialEq)]
630struct NearestPoint {
631    point: Point<Pixels>,
632    distance_squared: f32,
633}
634
635fn segment_nearest_point(
636    segment: &CanvasRouteSegment,
637    point: Point<Pixels>,
638) -> Option<NearestPoint> {
639    match segment {
640        CanvasRouteSegment::Line { from, to } => {
641            Some(point_to_line_segment_nearest_point(point, *from, *to))
642        }
643        CanvasRouteSegment::CubicBezier {
644            from,
645            control_1,
646            control_2,
647            to,
648        } => cubic_bezier_nearest_point(point, *from, *control_1, *control_2, *to),
649    }
650}
651
652fn cubic_bezier_nearest_point(
653    point: Point<Pixels>,
654    from: Point<Pixels>,
655    control_1: Point<Pixels>,
656    control_2: Point<Pixels>,
657    to: Point<Pixels>,
658) -> Option<NearestPoint> {
659    const STEPS: usize = 24;
660
661    let mut closest = None;
662    let mut previous = from;
663    for step in 1..=STEPS {
664        let t = step as f32 / STEPS as f32;
665        let current = cubic_bezier_point(from, control_1, control_2, to, t);
666        let nearest = point_to_line_segment_nearest_point(point, previous, current);
667        if closest
668            .is_none_or(|closest: NearestPoint| nearest.distance_squared < closest.distance_squared)
669        {
670            closest = Some(nearest);
671        }
672        previous = current;
673    }
674    closest
675}
676
677fn cubic_bezier_point(
678    from: Point<Pixels>,
679    control_1: Point<Pixels>,
680    control_2: Point<Pixels>,
681    to: Point<Pixels>,
682    t: f32,
683) -> Point<Pixels> {
684    let mt = 1.0 - t;
685    Point::new(
686        px(mt * mt * mt * from.x.as_f32()
687            + 3.0 * mt * mt * t * control_1.x.as_f32()
688            + 3.0 * mt * t * t * control_2.x.as_f32()
689            + t * t * t * to.x.as_f32()),
690        px(mt * mt * mt * from.y.as_f32()
691            + 3.0 * mt * mt * t * control_1.y.as_f32()
692            + 3.0 * mt * t * t * control_2.y.as_f32()
693            + t * t * t * to.y.as_f32()),
694    )
695}
696
697fn point_to_line_segment_nearest_point(
698    point: Point<Pixels>,
699    from: Point<Pixels>,
700    to: Point<Pixels>,
701) -> NearestPoint {
702    let point_x = point.x.as_f32();
703    let point_y = point.y.as_f32();
704    let ax = from.x.as_f32();
705    let ay = from.y.as_f32();
706    let bx = to.x.as_f32();
707    let by = to.y.as_f32();
708    let dx = bx - ax;
709    let dy = by - ay;
710    let length_squared = dx * dx + dy * dy;
711
712    if length_squared <= f32::EPSILON {
713        let dx = point_x - ax;
714        let dy = point_y - ay;
715        return NearestPoint {
716            point: from,
717            distance_squared: dx * dx + dy * dy,
718        };
719    }
720
721    let t = (((point_x - ax) * dx + (point_y - ay) * dy) / length_squared).clamp(0.0, 1.0);
722    let nearest_x = ax + t * dx;
723    let nearest_y = ay + t * dy;
724    let dx = point_x - nearest_x;
725    let dy = point_y - nearest_y;
726    NearestPoint {
727        point: Point::new(px(nearest_x), px(nearest_y)),
728        distance_squared: dx * dx + dy * dy,
729    }
730}
731
732fn record_options_match(record: &HitRecord, options: HitOptions) -> bool {
733    (options.include_hidden || !record.hidden)
734        && (options.include_locked || !record.locked)
735        && (options.include_handles || !matches!(record.target, HitTarget::Handle { .. }))
736}
737
738pub fn connection_hit_options() -> HitOptions {
739    HitOptions {
740        include_handles: true,
741        margin: px(4.0),
742        ..HitOptions::default()
743    }
744}
745
746#[cfg(test)]
747mod tests {
748    use super::*;
749    use crate::{
750        CanvasEdge, CanvasHandle, CanvasNode, CanvasNodeBoundsHitTest, CanvasNodeInteractionPolicy,
751        CanvasNodeKind, CanvasShape, CanvasStyle, HandleRole, test_support::document_fixture,
752    };
753    use open_gpui::{point, px, size};
754
755    #[test]
756    fn facts_use_same_endpoint_position_for_handles_and_node_centers() {
757        let mut node = CanvasNode::new("a", point(px(10.0), px(20.0)), size(px(40.0), px(60.0)));
758        node.handles
759            .push(CanvasHandle::new("out", point(px(40.0), px(30.0))));
760        let document = document_fixture().node(node).build();
761        let facts = CanvasGeometryFacts::new(&document);
762
763        assert_eq!(
764            facts
765                .endpoint_position(&CanvasEndpoint::new("a", None::<&str>))
766                .unwrap(),
767            point(px(30.0), px(50.0))
768        );
769        assert_eq!(
770            facts
771                .endpoint_position(&CanvasEndpoint::new("a", Some("out")))
772                .unwrap(),
773            point(px(50.0), px(50.0))
774        );
775    }
776
777    #[test]
778    fn facts_pick_connection_handles_by_role() {
779        let mut node = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
780        let mut target_only = CanvasHandle::new("in", point(px(100.0), px(50.0)));
781        target_only.role = HandleRole::Target;
782        node.handles.push(target_only);
783        let document = document_fixture().node(node).build();
784        let facts = CanvasGeometryFacts::new(&document);
785        let records = [HitRecord {
786            target: HitTarget::Handle {
787                node_id: "a".into(),
788                handle_id: "in".into(),
789            },
790            bounds: Bounds::centered_at(point(px(100.0), px(50.0)), size(px(12.0), px(12.0))),
791            z_index: 0,
792            hidden: false,
793            locked: false,
794        }];
795
796        assert_eq!(
797            facts.connection_endpoint_at(&records, CanvasConnectionEndpointRole::Target),
798            Some(CanvasEndpoint::new("a", Some("in")))
799        );
800        assert_eq!(
801            facts.connection_endpoint_at(&records, CanvasConnectionEndpointRole::Source),
802            None
803        );
804    }
805
806    #[test]
807    fn facts_materialize_hit_records_for_nodes_handles_shapes_and_edges() {
808        let mut node = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
809        node.handles
810            .push(CanvasHandle::new("out", point(px(10.0), px(5.0))));
811        let document = document_fixture()
812            .node(node)
813            .node(CanvasNode::new(
814                "b",
815                point(px(100.0), px(0.0)),
816                size(px(10.0), px(10.0)),
817            ))
818            .shape(CanvasShape::new(
819                "shape",
820                Bounds::new(point(px(0.0), px(40.0)), size(px(20.0), px(20.0))),
821            ))
822            .edge(CanvasEdge::new(
823                "a-b",
824                CanvasEndpoint::new("a", Some("out")),
825                CanvasEndpoint::new("b", None::<&str>),
826            ))
827            .build();
828
829        let records = CanvasGeometryFacts::new(&document).hit_records();
830        let targets = records
831            .into_iter()
832            .map(|record| record.target)
833            .collect::<Vec<_>>();
834
835        assert_eq!(
836            targets,
837            vec![
838                HitTarget::Node("a".into()),
839                HitTarget::Handle {
840                    node_id: "a".into(),
841                    handle_id: "out".into(),
842                },
843                HitTarget::Node("b".into()),
844                HitTarget::Shape("shape".into()),
845                HitTarget::Edge("a-b".into()),
846            ]
847        );
848    }
849
850    #[test]
851    fn resolved_edge_geometry_answers_nearest_point_and_hit() {
852        let mut edge = CanvasEdge::new(
853            "a-b",
854            CanvasEndpoint::new("a", None::<&str>),
855            CanvasEndpoint::new("b", None::<&str>),
856        );
857        edge.style = CanvasStyle {
858            stroke: Some("#0969da".to_string()),
859            stroke_width: px(4.0),
860            fill: None,
861        };
862        let document = document_fixture()
863            .node(CanvasNode::new(
864                "a",
865                point(px(0.0), px(0.0)),
866                size(px(10.0), px(10.0)),
867            ))
868            .node(CanvasNode::new(
869                "b",
870                point(px(100.0), px(0.0)),
871                size(px(10.0), px(10.0)),
872            ))
873            .edge(edge.clone())
874            .build();
875
876        let geometry = CanvasGeometryFacts::new(&document)
877            .edge_geometry(&edge)
878            .unwrap();
879
880        assert_eq!(geometry.hit_radius, px(6.0));
881        assert_eq!(
882            geometry.nearest_point(point(px(40.0), px(8.0))).unwrap(),
883            point(px(40.0), px(5.0))
884        );
885        assert!(geometry.contains_point(point(px(40.0), px(10.5)), Pixels::ZERO));
886        assert!(!geometry.contains_point(point(px(40.0), px(11.5)), Pixels::ZERO));
887        assert!(geometry.contains_point(point(px(40.0), px(11.5)), px(1.0)));
888    }
889
890    #[test]
891    fn record_intersects_bounds_uses_shape_interaction_policy() {
892        let mut group = CanvasShape::new(
893            "group",
894            Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
895        );
896        group.kind = CanvasKindRegistry::GROUP_SHAPE_KIND.to_string();
897        let document = document_fixture().shape(group).build();
898        let registry = CanvasKindRegistry::open();
899        let facts = CanvasGeometryFacts::with_kind_registry(&document, &registry);
900        let group_record = facts
901            .hit_records()
902            .into_iter()
903            .find(|record| record.target == HitTarget::Shape("group".into()))
904            .unwrap();
905
906        assert!(!facts.record_intersects_bounds(
907            &group_record,
908            Bounds::new(point(px(40.0), px(40.0)), size(px(20.0), px(20.0))),
909            HitOptions::default()
910        ));
911        assert!(facts.record_intersects_bounds(
912            &group_record,
913            Bounds::new(point(px(0.0), px(40.0)), size(px(8.0), px(20.0))),
914            HitOptions::default()
915        ));
916    }
917
918    #[test]
919    fn record_intersects_bounds_uses_node_bounds_interaction_policy() {
920        struct RightHalfBoundsKind;
921
922        impl CanvasNodeInteractionPolicy for RightHalfBoundsKind {
923            fn node_intersects_bounds(&self, hit: CanvasNodeBoundsHitTest<'_>) -> Option<bool> {
924                let active = Bounds::from_corners(
925                    Point::new(hit.bounds.center().x, hit.bounds.origin.y),
926                    Point::new(
927                        hit.bounds.origin.x + hit.bounds.size.width,
928                        hit.bounds.origin.y + hit.bounds.size.height,
929                    ),
930                );
931                Some(active.intersects(&hit.query_bounds))
932            }
933        }
934
935        let mut node = CanvasNode::new("node", point(px(0.0), px(0.0)), size(px(100.0), px(100.0)));
936        node.kind = "right-half-bounds".to_string();
937        let document = document_fixture().node(node).build();
938        let mut registry = CanvasKindRegistry::open();
939        registry.register_node_kind(
940            "right-half-bounds",
941            CanvasNodeKind::new().with_interaction_policy(RightHalfBoundsKind),
942        );
943        let facts = CanvasGeometryFacts::with_kind_registry(&document, &registry);
944        let node_record = facts
945            .hit_records()
946            .into_iter()
947            .find(|record| record.target == HitTarget::Node("node".into()))
948            .unwrap();
949
950        assert!(!facts.record_intersects_bounds(
951            &node_record,
952            Bounds::new(point(px(10.0), px(10.0)), size(px(20.0), px(20.0))),
953            HitOptions::default()
954        ));
955        assert!(facts.record_intersects_bounds(
956            &node_record,
957            Bounds::new(point(px(60.0), px(10.0)), size(px(20.0), px(20.0))),
958            HitOptions::default()
959        ));
960    }
961
962    #[test]
963    fn record_intersects_bounds_defaults_to_bounds_for_regular_shapes() {
964        let document = document_fixture()
965            .shape(CanvasShape::new(
966                "shape",
967                Bounds::new(point(px(10.0), px(10.0)), size(px(20.0), px(20.0))),
968            ))
969            .build();
970        let facts = CanvasGeometryFacts::new(&document);
971        let shape_record = facts
972            .hit_records()
973            .into_iter()
974            .find(|record| record.target == HitTarget::Shape("shape".into()))
975            .unwrap();
976
977        assert!(facts.record_intersects_bounds(
978            &shape_record,
979            Bounds::new(point(px(25.0), px(25.0)), size(px(20.0), px(20.0))),
980            HitOptions::default()
981        ));
982        assert!(!facts.record_intersects_bounds(
983            &shape_record,
984            Bounds::new(point(px(40.0), px(40.0)), size(px(20.0), px(20.0))),
985            HitOptions::default()
986        ));
987    }
988}