use std::{collections::HashMap, rc::Rc};
use itertools::Itertools;
use log::{debug, error};
use orrery_core::{
draw::{
Arrow, ArrowPath, ArrowStyle, ArrowWithText, PositionedArrowWithText, PositionedDrawable,
ShapeWithText,
},
geometry::{Bounds, Point},
identifier::Id,
semantic::{Node, Relation},
};
use crate::{
error::RenderError,
layout::{layer::ContentStack, positioning::LayoutBounds},
structure::ComponentGraph,
};
#[derive(Debug, Clone)]
pub struct Component<'a> {
node_id: Id, drawable: Rc<PositionedDrawable<ShapeWithText<'a>>>, }
impl<'a> Component<'a> {
pub fn new(node: &Node, shape_with_text: ShapeWithText<'a>, position: Point) -> Component<'a> {
let drawable = Rc::new(PositionedDrawable::new(shape_with_text).with_position(position));
Component {
node_id: node.id(),
drawable,
}
}
pub fn drawable(&self) -> &PositionedDrawable<ShapeWithText<'_>> {
&self.drawable
}
pub fn position(&self) -> Point {
self.drawable.position()
}
pub fn bounds(&self) -> Bounds {
self.drawable.bounds()
}
pub fn node_id(&self) -> Id {
self.node_id
}
pub fn find_intersection(&self, external_point: Point) -> Point {
self.drawable
.inner()
.find_intersection(self.position(), external_point)
}
}
pub trait ArrowPlacer {
fn place<'a>(
&self,
relations: &[&'a Relation],
source: &Component<'_>,
target: &Component<'_>,
) -> Vec<PositionedArrowWithText<'a>>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct StraightArrowPlacer;
impl StraightArrowPlacer {
fn place_one<'a>(
relation: &'a Relation,
source: &Component<'_>,
target: &Component<'_>,
) -> PositionedArrowWithText<'a> {
let arrow_def = Rc::clone(relation.arrow_definition());
let arrow = Arrow::new(arrow_def, relation.arrow_direction());
let arrow_with_text = ArrowWithText::new(arrow, relation.text());
let source_edge = source.find_intersection(target.position());
let target_edge = target.find_intersection(source.position());
let path = ArrowPath::straight(source_edge, target_edge);
PositionedArrowWithText::new(arrow_with_text, path)
}
}
impl ArrowPlacer for StraightArrowPlacer {
fn place<'a>(
&self,
relations: &[&'a Relation],
source: &Component<'_>,
target: &Component<'_>,
) -> Vec<PositionedArrowWithText<'a>> {
relations
.iter()
.map(|relation| {
let (rel_src, rel_tgt) = align_to_relation(relation, source, target);
Self::place_one(relation, rel_src, rel_tgt)
})
.collect()
}
}
#[derive(Debug, Clone, Copy)]
pub struct CurvedArrowPlacer {
lane_spacing: f32,
self_loop_radius: f32,
self_loop_spread: f32,
}
impl CurvedArrowPlacer {
const DEFAULT_LANE_SPACING: f32 = 22.0;
const DEFAULT_SELF_LOOP_RADIUS: f32 = 35.0;
const DEFAULT_SELF_LOOP_SPREAD: f32 = std::f32::consts::FRAC_PI_6;
pub fn new() -> Self {
Self {
lane_spacing: Self::DEFAULT_LANE_SPACING,
self_loop_radius: Self::DEFAULT_SELF_LOOP_RADIUS,
self_loop_spread: Self::DEFAULT_SELF_LOOP_SPREAD,
}
}
fn curved_arrow<'a>(
relation: &'a Relation,
source: &Component<'_>,
target: &Component<'_>,
lane_offset: f32,
) -> PositionedArrowWithText<'a> {
let Some((path, label_position)) = Self::lane_geometry(source, target, lane_offset) else {
return StraightArrowPlacer::place_one(relation, source, target);
};
let arrow_def = Rc::clone(relation.arrow_definition());
let arrow = Arrow::new(arrow_def, relation.arrow_direction());
let arrow_with_text = ArrowWithText::new(arrow, relation.text());
PositionedArrowWithText::new(arrow_with_text, path).with_text_position(Some(label_position))
}
fn lane_geometry(
source: &Component<'_>,
target: &Component<'_>,
lane_offset: f32,
) -> Option<(ArrowPath, Point)> {
let src_center = source.position();
let tgt_center = target.position();
let delta = tgt_center.sub_point(src_center);
let len = delta.hypot();
if len == 0.0 {
return None;
}
let perp = Point::new(
-delta.y() / len * lane_offset,
delta.x() / len * lane_offset,
);
let midpoint = src_center.midpoint(tgt_center).add_point(perp);
let src_edge = source.find_intersection(midpoint);
let tgt_edge = target.find_intersection(midpoint);
let (third, two_thirds) = line_segment_thirds(src_edge, tgt_edge);
let cp1 = third.add_point(perp);
let cp2 = two_thirds.add_point(perp);
let path = ArrowPath::new(src_edge, tgt_edge, vec![cp1, cp2]);
let label_position = cubic_bezier_midpoint(src_edge, cp1, cp2, tgt_edge);
Some((path, label_position))
}
fn lane_offset_at(&self, k: usize, n: usize, relation: &Relation, source_id: Id) -> f32 {
let offset = ((k as f32) - ((n - 1) as f32) / 2.0) * self.lane_spacing;
if relation.source() != source_id {
-offset
} else {
offset
}
}
fn self_loop_angle(k: usize, n: usize) -> f32 {
debug_assert_ne!(n, 0, "n must be greater than 0");
(k as f32) * std::f32::consts::TAU / (n as f32)
}
fn self_loop_geometry(&self, component: &Component<'_>, angle: f32) -> (ArrowPath, Point) {
let center = component.position();
let direction = Point::new(angle.cos(), angle.sin());
let component_size = component.bounds().to_size();
let far = (component_size.height().abs() + component_size.width().abs()) * 2.0;
let half_spread = self.self_loop_spread / 2.0;
let src_angle = angle - half_spread;
let dst_angle = angle + half_spread;
let src_dir = Point::new(src_angle.cos(), src_angle.sin());
let dst_dir = Point::new(dst_angle.cos(), dst_angle.sin());
let src_far = center.add_point(src_dir.scale(far));
let dst_far = center.add_point(dst_dir.scale(far));
let src_edge = component.find_intersection(src_far);
let dst_edge = component.find_intersection(dst_far);
let outward = direction.scale(self.self_loop_radius);
let cp1 = src_edge.add_point(outward);
let cp2 = dst_edge.add_point(outward);
let path = ArrowPath::new(src_edge, dst_edge, vec![cp1, cp2]);
let label_position = cubic_bezier_midpoint(src_edge, cp1, cp2, dst_edge);
(path, label_position)
}
fn self_loop_arrow<'a>(
&self,
relation: &'a Relation,
component: &Component<'_>,
angle: f32,
) -> PositionedArrowWithText<'a> {
let (path, label_position) = self.self_loop_geometry(component, angle);
let arrow_def = Rc::clone(relation.arrow_definition());
let arrow = Arrow::new(arrow_def, relation.arrow_direction());
let arrow_with_text = ArrowWithText::new(arrow, relation.text());
PositionedArrowWithText::new(arrow_with_text, path).with_text_position(Some(label_position))
}
}
impl Default for CurvedArrowPlacer {
fn default() -> Self {
Self::new()
}
}
impl ArrowPlacer for CurvedArrowPlacer {
fn place<'a>(
&self,
relations: &[&'a Relation],
source: &Component<'_>,
target: &Component<'_>,
) -> Vec<PositionedArrowWithText<'a>> {
let n = relations.len();
if source.node_id() == target.node_id() {
relations
.iter()
.enumerate()
.map(|(k, relation)| {
let angle = Self::self_loop_angle(k, n);
self.self_loop_arrow(relation, source, angle)
})
.collect()
} else {
relations
.iter()
.enumerate()
.map(|(k, relation)| {
let (rel_src, rel_tgt) = align_to_relation(relation, source, target);
let lane_offset = self.lane_offset_at(k, n, relation, source.node_id());
if lane_offset == 0.0 {
return StraightArrowPlacer::place_one(relation, rel_src, rel_tgt);
}
Self::curved_arrow(relation, rel_src, rel_tgt, lane_offset)
})
.collect()
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct OrthogonalArrowPlacer;
impl OrthogonalArrowPlacer {
fn path(source: Point, target: Point) -> ArrowPath {
let abs_dist = target.sub_point(source).abs();
let mid = source.midpoint(target);
let cps = if abs_dist.x() > abs_dist.y() {
vec![mid.with_y(source.y()), mid.with_y(target.y())]
} else {
vec![mid.with_x(source.x()), mid.with_x(target.x())]
};
ArrowPath::new(source, target, cps)
}
fn place_one<'a>(
relation: &'a Relation,
source: &Component<'_>,
target: &Component<'_>,
) -> PositionedArrowWithText<'a> {
let arrow_def = Rc::clone(relation.arrow_definition());
let arrow = Arrow::new(arrow_def, relation.arrow_direction());
let arrow_with_text = ArrowWithText::new(arrow, relation.text());
let source_edge = source.find_intersection(target.position());
let target_edge = target.find_intersection(source.position());
let path = Self::path(source_edge, target_edge);
PositionedArrowWithText::new(arrow_with_text, path)
}
}
impl ArrowPlacer for OrthogonalArrowPlacer {
fn place<'a>(
&self,
relations: &[&'a Relation],
source: &Component<'_>,
target: &Component<'_>,
) -> Vec<PositionedArrowWithText<'a>> {
relations
.iter()
.map(|relation| {
let (rel_src, rel_tgt) = align_to_relation(relation, source, target);
Self::place_one(relation, rel_src, rel_tgt)
})
.collect()
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SmartArrowPlacer {
straight: StraightArrowPlacer,
curved: CurvedArrowPlacer,
orthogonal: OrthogonalArrowPlacer,
}
impl SmartArrowPlacer {
pub fn new() -> Self {
Self::default()
}
}
impl ArrowPlacer for SmartArrowPlacer {
fn place<'a>(
&self,
relations: &[&'a Relation],
source: &Component<'_>,
target: &Component<'_>,
) -> Vec<PositionedArrowWithText<'a>> {
if source.node_id() == target.node_id() {
return self.curved.place(relations, source, target);
}
let grouped = relations
.iter()
.copied()
.into_group_map_by(|r| r.arrow_definition().style());
grouped
.into_iter()
.flat_map(|(style, relations)| match style {
ArrowStyle::Straight => self.straight.place(&relations, source, target),
ArrowStyle::Curved => self.curved.place(&relations, source, target),
ArrowStyle::Orthogonal => self.orthogonal.place(&relations, source, target),
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct Layout<'a> {
components: Vec<Component<'a>>,
relations: Vec<PositionedArrowWithText<'a>>,
bounds: Bounds,
}
impl<'a> Layout<'a> {
pub fn new(
components: Vec<Component<'a>>,
relations: Vec<PositionedArrowWithText<'a>>,
) -> Self {
let bounds = if components.is_empty() {
Bounds::default()
} else {
components
.iter()
.skip(1)
.fold(components[0].bounds(), |acc, comp| {
acc.merge(&comp.bounds())
})
};
Self {
components,
relations,
bounds,
}
}
pub fn components(&self) -> &[Component<'a>] {
&self.components
}
pub fn relations(&self) -> &[PositionedArrowWithText<'a>] {
&self.relations
}
}
impl<'a> LayoutBounds for Layout<'a> {
fn layout_bounds(&self) -> Bounds {
self.bounds
}
}
pub fn adjust_positioned_contents_offset<'a>(
content_stack: &mut ContentStack<Layout>,
graph: &'a ComponentGraph<'a, '_>,
) -> Result<(), RenderError> {
let container_indices: HashMap<_, _> = graph
.containment_scopes()
.enumerate()
.filter_map(|(idx, scope)| scope.container().map(|container| (container, idx)))
.collect();
for (source_idx, source_scope) in graph.containment_scopes().enumerate().rev() {
for (node_id, destination_idx) in source_scope.node_ids().filter_map(|node_id| {
container_indices
.get(&node_id)
.map(|&destination_idx| (node_id, destination_idx))
}) {
if source_idx == destination_idx {
error!(index = source_idx; "Source and destination indices are the same");
continue;
}
let source = content_stack.get_unchecked(source_idx);
let node = graph.node_by_id(node_id).ok_or_else(|| {
RenderError::Layout(format!(
"Node with id {node_id} not found in graph during layout adjustment"
))
})?;
let source_component = source
.content()
.components()
.iter()
.find(|component| component.node_id == node.id())
.ok_or_else(|| {
RenderError::Layout(format!(
"Component with id {node} not found in source layer {source_idx}"
))
})?;
let target_offset = source
.offset()
.add_point(source_component.bounds().min_point())
.add_point(
source_component
.drawable
.inner()
.shape_to_inner_content_min_point(),
); debug!(
node_id:% = node,
source_offset:? = source.offset();
"Adjusting positioned content offset [source]",
);
let target = content_stack.get_mut_unchecked(destination_idx);
debug!(
node_id:% = node,
original_offset:? = target.offset(),
new_offset:? = target_offset;
"Adjusting positioned content offset [target]",
);
target.set_offset(target_offset);
}
}
Ok(())
}
fn align_to_relation<'a, 'b>(
relation: &Relation,
source: &'b Component<'a>,
target: &'b Component<'a>,
) -> (&'b Component<'a>, &'b Component<'a>) {
if relation.source() == source.node_id() {
(source, target)
} else {
(target, source)
}
}
fn line_segment_thirds(start: Point, end: Point) -> (Point, Point) {
let dx = (end.x() - start.x()) / 3.0;
let dy = (end.y() - start.y()) / 3.0;
(
Point::new(start.x() + dx, start.y() + dy),
Point::new(start.x() + 2.0 * dx, start.y() + 2.0 * dy),
)
}
fn cubic_bezier_midpoint(start: Point, cp1: Point, cp2: Point, end: Point) -> Point {
Point::new(
0.125 * start.x() + 0.375 * cp1.x() + 0.375 * cp2.x() + 0.125 * end.x(),
0.125 * start.y() + 0.375 * cp1.y() + 0.375 * cp2.y() + 0.125 * end.y(),
)
}
#[cfg(test)]
mod tests {
use std::f32::consts::{FRAC_PI_2, PI};
use float_cmp::{approx_eq, assert_approx_eq};
use orrery_core::{
draw::{ArrowDefinition, ArrowDirection, RectangleDefinition, Shape, ShapeDefinition},
semantic::Block,
};
use super::*;
fn assert_point_approx_eq(actual: Point, expected: Point) {
assert!(
approx_eq!(f32, actual.x(), expected.x(), epsilon = 0.01)
&& approx_eq!(f32, actual.y(), expected.y(), epsilon = 0.01),
"expected ({}, {}), got ({}, {})",
expected.x(),
expected.y(),
actual.x(),
actual.y(),
);
}
fn make_relation(source: Id, target: Id) -> Relation {
Relation::new(
source,
target,
ArrowDirection::Forward,
None,
Rc::new(ArrowDefinition::default()),
)
}
fn make_relation_with_style(source: Id, target: Id, style: ArrowStyle) -> Relation {
let mut arrow_def = ArrowDefinition::default();
arrow_def.set_style(style);
Relation::new(
source,
target,
ArrowDirection::Forward,
None,
Rc::new(arrow_def),
)
}
fn make_node(name: &str) -> Node {
let id = Id::new(name);
let shape_def = Rc::new(Box::new(RectangleDefinition::new()) as Box<dyn ShapeDefinition>);
Node::new(id, None, Block::None, shape_def)
}
fn make_component<'a>(node: &'a Node, position: Point) -> Component<'a> {
let shape = Shape::new(Rc::clone(node.shape_definition()));
let shape_with_text = ShapeWithText::new(shape, None);
Component::new(node, shape_with_text, position)
}
#[test]
fn line_segment_thirds_returns_third_points() {
let (cp1, cp2) = line_segment_thirds(Point::new(0.0, 0.0), Point::new(30.0, 60.0));
assert_eq!(cp1, Point::new(10.0, 20.0));
assert_eq!(cp2, Point::new(20.0, 40.0));
}
#[test]
fn cubic_bezier_midpoint_at_half() {
let s = Point::new(0.0, 0.0);
let cp1 = Point::new(30.0, 0.0);
let cp2 = Point::new(60.0, 0.0);
let d = Point::new(90.0, 0.0);
let mid = cubic_bezier_midpoint(s, cp1, cp2, d);
assert_eq!(mid, Point::new(45.0, 0.0));
}
#[test]
fn lane_offset_at_assigns_symmetric_lanes() {
let a_id = Id::new("a");
let b_id = Id::new("b");
let r0 = make_relation(a_id, b_id);
let r1 = make_relation(a_id, b_id);
let router = CurvedArrowPlacer {
lane_spacing: 10.0,
..CurvedArrowPlacer::new()
};
let off0 = router.lane_offset_at(0, 2, &r0, a_id);
let off1 = router.lane_offset_at(1, 2, &r1, a_id);
assert_eq!(off0, -5.0);
assert_eq!(off1, 5.0);
}
#[test]
fn lane_offset_at_three_relations_has_zero_median() {
let a_id = Id::new("a");
let b_id = Id::new("b");
let r = make_relation(a_id, b_id);
let router = CurvedArrowPlacer {
lane_spacing: 10.0,
..CurvedArrowPlacer::new()
};
assert_eq!(router.lane_offset_at(1, 3, &r, a_id), 0.0);
assert_eq!(router.lane_offset_at(2, 3, &r, a_id), 10.0);
}
#[test]
fn lane_offset_at_single_relation_is_zero() {
let a_id = Id::new("a");
let b_id = Id::new("b");
let r = make_relation(a_id, b_id);
let router = CurvedArrowPlacer::new();
assert_eq!(router.lane_offset_at(0, 1, &r, a_id), 0.0);
}
#[test]
fn lane_geometry_emits_cubic_bezier_with_two_control_points() {
let a_node = make_node("a");
let b_node = make_node("b");
let a = make_component(&a_node, Point::new(0.0, 0.0));
let b = make_component(&b_node, Point::new(1000.0, 0.0));
let (path, label) =
CurvedArrowPlacer::lane_geometry(&a, &b, 18.0).expect("non-degenerate centerline");
assert_eq!(path.control_points().len(), 2);
assert_point_approx_eq(path.source(), Point::new(6.0, 0.216));
assert_point_approx_eq(path.destination(), Point::new(994.0, 0.216));
assert_point_approx_eq(path.control_points()[0], Point::new(335.33, 18.216));
assert_point_approx_eq(path.control_points()[1], Point::new(664.67, 18.216));
assert_point_approx_eq(label, Point::new(500.0, 13.716));
}
#[test]
fn lane_geometry_opposite_offsets_produce_mirrored_control_points() {
let a_node = make_node("a");
let b_node = make_node("b");
let a = make_component(&a_node, Point::new(0.0, 0.0));
let b = make_component(&b_node, Point::new(1000.0, 0.0));
let (path_pos, _) = CurvedArrowPlacer::lane_geometry(&a, &b, 18.0).unwrap();
let (path_neg, _) = CurvedArrowPlacer::lane_geometry(&a, &b, -18.0).unwrap();
let cp1_pos = path_pos.control_points()[0];
let cp1_neg = path_neg.control_points()[0];
assert_point_approx_eq(cp1_neg, Point::new(cp1_pos.x(), -cp1_pos.y()));
}
#[test]
fn lane_geometry_offset_scales_with_magnitude() {
let a_node = make_node("a");
let b_node = make_node("b");
let a = make_component(&a_node, Point::new(0.0, 0.0));
let b = make_component(&b_node, Point::new(1000.0, 0.0));
let (path_small, _) = CurvedArrowPlacer::lane_geometry(&a, &b, 10.0).unwrap();
let (path_large, _) = CurvedArrowPlacer::lane_geometry(&a, &b, 40.0).unwrap();
let cp1_small = path_small.control_points()[0];
let cp1_large = path_large.control_points()[0];
assert_approx_eq!(f32, cp1_large.y() / cp1_small.y(), 4.0);
}
#[test]
fn lane_geometry_returns_none_for_zero_length_centerline() {
let a_node = make_node("a");
let a = make_component(&a_node, Point::new(0.0, 0.0));
assert!(CurvedArrowPlacer::lane_geometry(&a, &a, 18.0).is_none());
}
#[test]
fn self_loop_angle_distributes_evenly() {
assert_approx_eq!(f32, CurvedArrowPlacer::self_loop_angle(0, 1), 0.0);
assert_approx_eq!(f32, CurvedArrowPlacer::self_loop_angle(0, 2), 0.0);
assert_approx_eq!(f32, CurvedArrowPlacer::self_loop_angle(1, 2), PI);
assert_approx_eq!(f32, CurvedArrowPlacer::self_loop_angle(0, 4), 0.0);
assert_approx_eq!(f32, CurvedArrowPlacer::self_loop_angle(1, 4), FRAC_PI_2);
assert_approx_eq!(f32, CurvedArrowPlacer::self_loop_angle(2, 4), PI);
assert_approx_eq!(
f32,
CurvedArrowPlacer::self_loop_angle(3, 4),
3.0 * FRAC_PI_2
);
}
#[test]
fn self_loop_geometry_source_and_dest_on_boundary() {
let node = make_node("a");
let comp = make_component(&node, Point::new(50.0, 50.0));
let router = CurvedArrowPlacer::new();
let (path, _) = router.self_loop_geometry(&comp, 0.0);
assert_approx_eq!(f32, path.source().x(), 56.0);
assert_approx_eq!(f32, path.destination().x(), 56.0);
assert!(path.source().y() < 50.0);
assert!(path.destination().y() > 50.0);
}
#[test]
fn self_loop_geometry_control_points_outside_bounds() {
let node = make_node("a");
let comp = make_component(&node, Point::new(50.0, 50.0));
let router = CurvedArrowPlacer::new();
let bounds = comp.bounds();
let (path, _) = router.self_loop_geometry(&comp, 0.0);
for cp in path.control_points() {
assert!(
cp.x() > bounds.max_x(),
"control point x={} should exceed bounds max_x={}",
cp.x(),
bounds.max_x()
);
}
}
#[test]
fn self_loop_geometry_label_outside_bounds() {
let node = make_node("a");
let comp = make_component(&node, Point::new(50.0, 50.0));
let router = CurvedArrowPlacer::new();
let bounds = comp.bounds();
let (_, label) = router.self_loop_geometry(&comp, 0.0);
assert!(
label.x() > bounds.max_x(),
"label x={} should exceed bounds max_x={}",
label.x(),
bounds.max_x()
);
}
#[test]
fn self_loop_geometry_different_angles_produce_distinct_paths() {
let node = make_node("a");
let comp = make_component(&node, Point::new(50.0, 50.0));
let router = CurvedArrowPlacer::new();
let (path_right, _) = router.self_loop_geometry(&comp, 0.0);
let (path_left, _) = router.self_loop_geometry(&comp, PI);
assert!(path_right.source().x() > 50.0);
assert!(path_left.source().x() < 50.0);
}
#[test]
fn place_produces_one_arrow_per_relation() {
let a_node = make_node("a");
let b_node = make_node("b");
let a = make_component(&a_node, Point::new(0.0, 0.0));
let b = make_component(&b_node, Point::new(100.0, 0.0));
let r1 = make_relation(a.node_id(), b.node_id());
let r2 = make_relation(a.node_id(), b.node_id());
let r3 = make_relation(b.node_id(), a.node_id());
let router = CurvedArrowPlacer::new();
let out = router.place(&[&r1, &r2, &r3], &a, &b);
assert_eq!(out.len(), 3);
let r4 = make_relation(a.node_id(), a.node_id());
let r5 = make_relation(a.node_id(), a.node_id());
let out_self = router.place(&[&r4, &r5], &a, &a);
assert_eq!(out_self.len(), 2);
}
#[test]
fn orthogonal_path_horizontal_first_when_wider() {
let source = Point::new(0.0, 0.0);
let target = Point::new(100.0, 20.0);
let path = OrthogonalArrowPlacer::path(source, target);
let cps = path.control_points();
assert_eq!(cps.len(), 2);
assert_point_approx_eq(cps[0], Point::new(50.0, 0.0));
assert_point_approx_eq(cps[1], Point::new(50.0, 20.0));
}
#[test]
fn orthogonal_path_vertical_first_when_taller() {
let source = Point::new(0.0, 0.0);
let target = Point::new(20.0, 100.0);
let path = OrthogonalArrowPlacer::path(source, target);
let cps = path.control_points();
assert_eq!(cps.len(), 2);
assert_point_approx_eq(cps[0], Point::new(0.0, 50.0));
assert_point_approx_eq(cps[1], Point::new(20.0, 50.0));
}
#[test]
fn orthogonal_place_produces_one_arrow_per_relation() {
let a_node = make_node("a");
let b_node = make_node("b");
let a = make_component(&a_node, Point::new(0.0, 0.0));
let b = make_component(&b_node, Point::new(100.0, 0.0));
let r1 = make_relation(a.node_id(), b.node_id());
let r2 = make_relation(b.node_id(), a.node_id());
let out = OrthogonalArrowPlacer.place(&[&r1, &r2], &a, &b);
assert_eq!(out.len(), 2);
for arrow in &out {
assert_eq!(arrow.path().control_points().len(), 2);
}
}
#[test]
fn smart_place_returns_one_arrow_per_relation_for_mixed_styles() {
let a_node = make_node("a");
let b_node = make_node("b");
let a = make_component(&a_node, Point::new(0.0, 0.0));
let b = make_component(&b_node, Point::new(100.0, 0.0));
let r_straight = make_relation_with_style(a.node_id(), b.node_id(), ArrowStyle::Straight);
let r_curved = make_relation_with_style(a.node_id(), b.node_id(), ArrowStyle::Curved);
let r_orthogonal =
make_relation_with_style(a.node_id(), b.node_id(), ArrowStyle::Orthogonal);
let placer = SmartArrowPlacer::new();
let out = placer.place(&[&r_straight, &r_curved, &r_orthogonal], &a, &b);
assert_eq!(out.len(), 3);
}
#[test]
fn smart_place_routes_self_loops_through_curved_placer() {
let a_node = make_node("a");
let a = make_component(&a_node, Point::new(50.0, 50.0));
let r = make_relation_with_style(a.node_id(), a.node_id(), ArrowStyle::Orthogonal);
let placer = SmartArrowPlacer::new();
let out = placer.place(&[&r], &a, &a);
assert_eq!(out.len(), 1);
assert_eq!(out[0].path().control_points().len(), 2);
}
}