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, ®istry);
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, ®istry);
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}