use std::{collections::HashMap, rc::Rc};
use log::warn;
use orrery_core::{
draw,
geometry::{Bounds, Point, Size},
identifier::Id,
semantic,
};
use crate::layout::{component, positioning::LayoutBounds};
#[derive(Debug, Clone)]
pub struct Participant<'a> {
component: component::Component<'a>,
lifeline: draw::PositionedDrawable<draw::Lifeline>,
}
impl<'a> Participant<'a> {
pub fn new(
component: component::Component<'a>,
lifeline: draw::PositionedDrawable<draw::Lifeline>,
) -> Self {
Self {
component,
lifeline,
}
}
pub fn component(&self) -> &component::Component<'_> {
&self.component
}
pub fn lifeline(&self) -> &draw::PositionedDrawable<draw::Lifeline> {
&self.lifeline
}
}
#[derive(Debug, Clone)]
pub struct Message<'a> {
source: Id,
target: Id,
y_position: f32,
arrow_with_text: draw::ArrowWithText<'a>,
}
impl<'a> Message<'a> {
pub fn from_ast(relation: &'a semantic::Relation, source: Id, target: Id) -> Self {
let arrow_def = Rc::clone(relation.arrow_definition());
let arrow = draw::Arrow::new(arrow_def, relation.arrow_direction());
let arrow_with_text = draw::ArrowWithText::new(arrow, relation.text());
Self {
source,
target,
y_position: 0.0,
arrow_with_text,
}
}
pub fn min_size(&self) -> Size {
self.arrow_with_text.min_size()
}
pub fn set_y_position(&mut self, y_position: f32) {
self.y_position = y_position;
}
pub fn arrow_with_text(&self) -> &draw::ArrowWithText<'a> {
&self.arrow_with_text
}
pub fn source(&self) -> Id {
self.source
}
pub fn target(&self) -> Id {
self.target
}
pub fn y_position(&self) -> f32 {
self.y_position
}
}
#[derive(Debug, Clone)]
pub struct ActivationBox {
participant_id: Id,
center_y: f32,
drawable: draw::ActivationBox,
}
#[derive(Debug, Clone)]
pub struct ActivationTiming {
participant_id: Id,
start_y: f32,
nesting_level: u32,
definition: Rc<draw::ActivationBoxDefinition>,
}
impl ActivationTiming {
pub fn new(
participant_id: Id,
start_y: f32,
nesting_level: u32,
definition: Rc<draw::ActivationBoxDefinition>,
) -> Self {
Self {
participant_id,
start_y,
nesting_level,
definition,
}
}
pub fn to_activation_box(&self, end_y: f32) -> ActivationBox {
const EDGE_CASE_BUFFER: f32 = 15.0;
let end_y = if end_y <= self.start_y {
self.start_y + EDGE_CASE_BUFFER
} else {
end_y
};
let center_y = (self.start_y() + end_y) / 2.0;
let height = end_y - self.start_y();
let drawable =
draw::ActivationBox::new(Rc::clone(&self.definition), height, self.nesting_level());
ActivationBox {
participant_id: self.participant_id(),
center_y,
drawable,
}
}
fn participant_id(&self) -> Id {
self.participant_id
}
fn start_y(&self) -> f32 {
self.start_y
}
fn nesting_level(&self) -> u32 {
self.nesting_level
}
}
impl ActivationBox {
pub fn participant_id(&self) -> Id {
self.participant_id
}
pub fn center_y(&self) -> f32 {
self.center_y
}
pub fn drawable(&self) -> &draw::ActivationBox {
&self.drawable
}
fn calculate_bounds(&self, participant_position: Point) -> Bounds {
let position_with_center_y = participant_position.with_y(self.center_y);
self.drawable.calculate_bounds(position_with_center_y)
}
fn is_active_at_y(&self, y: f32) -> bool {
let half_height = self.drawable.height() / 2.0;
let min_y = self.center_y - half_height;
let max_y = self.center_y + half_height;
y >= min_y && y <= max_y
}
fn intersection_x(&self, participant_position: Point, target_x: f32) -> f32 {
let bounds = self.calculate_bounds(participant_position);
if target_x > participant_position.x() {
bounds.max_x()
} else {
bounds.min_x()
}
}
}
pub struct FragmentTiming<'a> {
start_y: f32,
min_x: f32,
max_x: f32,
fragment: &'a semantic::Fragment,
active_section: Option<(&'a semantic::FragmentSection, f32)>,
sections: Vec<draw::FragmentSection>,
}
impl<'a> FragmentTiming<'a> {
pub fn new(fragment: &'a semantic::Fragment, start_y: f32) -> Self {
Self {
start_y,
min_x: f32::MAX,
max_x: f32::MIN,
fragment,
active_section: None,
sections: Vec::new(),
}
}
pub fn start_section(&mut self, section: &'a semantic::FragmentSection, start_y: f32) {
#[cfg(debug_assertions)]
assert!(self.active_section.is_none());
self.active_section = Some((section, start_y));
}
pub fn end_section(&mut self, end_y: f32) -> Result<(), &'static str> {
let (ast_section, start_y) = self
.active_section
.take()
.ok_or("There is no active fragment section")?;
let section = draw::FragmentSection::new(
ast_section.title().map(|title| title.to_string()),
end_y - start_y,
);
self.sections.push(section);
Ok(())
}
pub fn update_x(&mut self, source_x: f32, target_x: f32) {
self.min_x = self.min_x.min(source_x.min(target_x));
self.max_x = self.max_x.max(source_x.max(target_x));
}
pub fn section_header_height(&self, section: &semantic::FragmentSection) -> f32 {
let definition = self.fragment.definition();
let section_header_height = definition.section_header_size(section.title()).height();
if self.sections.is_empty() {
let fragment_header_height = definition.header_size(self.fragment.operation()).height();
section_header_height.max(fragment_header_height)
} else {
section_header_height
}
}
pub fn bottom_padding(&self) -> f32 {
self.fragment.definition().bottom_padding()
}
pub fn into_fragment(self, end_y: f32) -> draw::PositionedDrawable<draw::Fragment> {
#[cfg(debug_assertions)]
assert!(self.active_section.is_none());
let drawable = draw::Fragment::new(
Rc::clone(self.fragment.definition()),
self.fragment.operation().to_string(),
self.sections,
Size::new(self.max_x - self.min_x, end_y - self.start_y),
);
let center_x = (self.min_x + self.max_x) / 2.0;
let center_y = (self.start_y + end_y) / 2.0;
let position = Point::new(center_x, center_y);
draw::PositionedDrawable::new(drawable).with_position(position)
}
}
pub fn find_active_activation_box_for_participant(
activation_boxes: &[ActivationBox],
participant_id: Id,
message_y: f32,
) -> Option<&ActivationBox> {
if activation_boxes.is_empty() {
return None;
}
if !message_y.is_finite() {
warn!("Invalid message_y coordinate: {message_y}. Skipping activation box search.");
return None;
}
let mut active_boxes: Vec<&ActivationBox> = activation_boxes
.iter()
.filter(|activation_box| activation_box.participant_id() == participant_id)
.filter(|activation_box| activation_box.is_active_at_y(message_y))
.collect();
if active_boxes.is_empty() {
return None;
}
active_boxes.sort_by_key(|activation_box| activation_box.drawable().nesting_level());
active_boxes.last().copied()
}
pub fn calculate_message_endpoint_x(
activation_boxes: &[ActivationBox],
participant: &component::Component,
participant_id: Id,
message_y: f32,
target_x: f32,
) -> f32 {
if let Some(activation_box) =
find_active_activation_box_for_participant(activation_boxes, participant_id, message_y)
{
activation_box.intersection_x(participant.position(), target_x)
} else {
participant.position().x()
}
}
#[derive(Debug, Clone)]
pub struct Layout<'a> {
participants: HashMap<Id, Participant<'a>>,
messages: Vec<Message<'a>>,
activations: Vec<ActivationBox>,
fragments: Vec<draw::PositionedDrawable<draw::Fragment>>,
notes: Vec<draw::PositionedDrawable<draw::Note>>,
max_lifeline_end: f32, bounds: Bounds,
}
impl<'a> Layout<'a> {
pub fn new(
participants: HashMap<Id, Participant<'a>>,
messages: Vec<Message<'a>>,
activations: Vec<ActivationBox>,
fragments: Vec<draw::PositionedDrawable<draw::Fragment>>,
notes: Vec<draw::PositionedDrawable<draw::Note>>,
max_lifeline_end: f32,
) -> Self {
let bounds = participants
.values()
.map(|participant| participant.component().bounds())
.reduce(|acc, bounds| acc.merge(&bounds))
.unwrap_or_default()
.with_max_y(max_lifeline_end);
Self {
participants,
messages,
activations,
fragments,
notes,
max_lifeline_end,
bounds,
}
}
pub fn participants(&self) -> &HashMap<Id, Participant<'a>> {
&self.participants
}
pub fn messages(&self) -> &[Message<'a>] {
&self.messages
}
pub fn activations(&self) -> &[ActivationBox] {
&self.activations
}
pub fn fragments(&self) -> &[draw::PositionedDrawable<draw::Fragment>] {
&self.fragments
}
pub fn notes(&self) -> &[draw::PositionedDrawable<draw::Note>] {
&self.notes
}
pub fn max_lifeline_end(&self) -> f32 {
self.max_lifeline_end
}
}
impl<'a> LayoutBounds for Layout<'a> {
fn layout_bounds(&self) -> Bounds {
self.bounds
}
}
#[cfg(test)]
mod tests {
use super::*;
use orrery_core::draw::Drawable;
#[test]
fn test_activation_box_is_active_at_y() {
let definition = draw::ActivationBoxDefinition::default();
let drawable = draw::ActivationBox::new(Rc::new(definition), 20.0, 0);
let activation_box = ActivationBox {
participant_id: Id::new("test"),
center_y: 100.0,
drawable,
};
assert!(activation_box.is_active_at_y(90.0)); assert!(activation_box.is_active_at_y(100.0)); assert!(activation_box.is_active_at_y(110.0));
assert!(!activation_box.is_active_at_y(89.9));
assert!(!activation_box.is_active_at_y(110.1));
}
#[test]
fn test_activation_box_get_intersection_x() {
let definition = draw::ActivationBoxDefinition::default();
let drawable = draw::ActivationBox::new(Rc::new(definition), 20.0, 0);
let activation_box = ActivationBox {
participant_id: Id::new("test"),
center_y: 100.0,
drawable,
};
let participant_position = Point::new(50.0, 80.0);
let rightward_x = activation_box.intersection_x(participant_position, 60.0);
assert_eq!(rightward_x, 54.0);
let leftward_x = activation_box.intersection_x(participant_position, 40.0);
assert_eq!(leftward_x, 46.0); }
#[test]
fn test_activation_box_nesting_offset() {
let definition = draw::ActivationBoxDefinition::default();
let drawable = draw::ActivationBox::new(Rc::new(definition), 20.0, 2);
let activation_box = ActivationBox {
participant_id: Id::new("test"),
center_y: 100.0,
drawable,
};
let participant_position = Point::new(50.0, 80.0);
let bounds = activation_box.calculate_bounds(participant_position);
assert_eq!(bounds.min_x(), 54.0);
assert_eq!(bounds.max_x(), 62.0);
}
#[test]
fn test_find_active_activation_box_for_participant() {
let drawable1 =
draw::ActivationBox::new(Rc::new(draw::ActivationBoxDefinition::default()), 20.0, 0);
let activation_box1 = ActivationBox {
participant_id: Id::new("test"),
center_y: 100.0,
drawable: drawable1,
};
let drawable2 =
draw::ActivationBox::new(Rc::new(draw::ActivationBoxDefinition::default()), 10.0, 1);
let activation_box2 = ActivationBox {
participant_id: Id::new("test"),
center_y: 100.0,
drawable: drawable2,
};
let drawable3 =
draw::ActivationBox::new(Rc::new(draw::ActivationBoxDefinition::default()), 20.0, 0);
let activation_box3 = ActivationBox {
participant_id: Id::new("test"),
center_y: 130.0,
drawable: drawable3,
};
let activation_boxes = vec![activation_box1, activation_box2, activation_box3];
let result =
find_active_activation_box_for_participant(&activation_boxes, Id::new("test"), 100.0);
assert!(result.is_some());
assert_eq!(result.unwrap().drawable().nesting_level(), 1);
let result =
find_active_activation_box_for_participant(&activation_boxes, Id::new("test"), 92.0);
assert!(result.is_some());
assert_eq!(result.unwrap().drawable().nesting_level(), 0);
let result =
find_active_activation_box_for_participant(&activation_boxes, Id::new("test"), 80.0);
assert!(result.is_none());
let result =
find_active_activation_box_for_participant(&activation_boxes, Id::new("test"), 130.0);
assert!(result.is_some());
assert_eq!(result.unwrap().participant_id(), "test");
let result = find_active_activation_box_for_participant(
&activation_boxes,
Id::new("invalid"),
100.0,
);
assert!(result.is_none());
}
#[test]
fn test_find_active_activation_box_edge_cases() {
let result = find_active_activation_box_for_participant(&[], Id::new("test"), 100.0);
assert!(result.is_none());
let boxes: Vec<ActivationBox> = (0..5)
.map(|i| {
let definition = draw::ActivationBoxDefinition::default();
let drawable = draw::ActivationBox::new(Rc::new(definition), 20.0, i);
ActivationBox {
participant_id: Id::new("test"),
center_y: 100.0,
drawable,
}
})
.collect();
let result = find_active_activation_box_for_participant(&boxes, Id::new("test"), 100.0);
assert!(result.is_some());
assert_eq!(result.unwrap().drawable().nesting_level(), 4);
}
#[test]
fn test_find_active_activation_box_error_handling() {
let definition = draw::ActivationBoxDefinition::default();
let drawable = draw::ActivationBox::new(Rc::new(definition), 20.0, 0);
let activation_box = ActivationBox {
participant_id: Id::new("test"),
center_y: 100.0,
drawable,
};
let activation_boxes = vec![activation_box];
let result = find_active_activation_box_for_participant(
&activation_boxes,
Id::new("test"),
f32::NAN,
);
assert!(result.is_none());
let result = find_active_activation_box_for_participant(
&activation_boxes,
Id::new("test"),
f32::INFINITY,
);
assert!(result.is_none());
let result = find_active_activation_box_for_participant(
&activation_boxes,
Id::new("test"),
f32::NEG_INFINITY,
);
assert!(result.is_none());
let result =
find_active_activation_box_for_participant(&activation_boxes, Id::new("test"), 100.0);
assert!(result.is_some());
}
#[test]
fn test_message_endpoint_fallback_behavior() {
let empty_boxes: Vec<ActivationBox> = vec![];
let result =
find_active_activation_box_for_participant(&empty_boxes, Id::new("test_0"), 100.0);
assert!(result.is_none());
let definition = draw::ActivationBoxDefinition::default();
let drawable = draw::ActivationBox::new(Rc::new(definition), 20.0, 0);
let activation_box = ActivationBox {
participant_id: Id::new("test_1"), center_y: 100.0,
drawable,
};
let activation_boxes = vec![activation_box];
let result =
find_active_activation_box_for_participant(&activation_boxes, Id::new("test_0"), 100.0);
assert!(result.is_none());
let result =
find_active_activation_box_for_participant(&activation_boxes, Id::new("test_1"), 200.0);
assert!(result.is_none());
}
#[test]
fn test_fragment_timing_lifecycle() {
let fragment_def = Rc::new(draw::FragmentDefinition::default());
let section1 = semantic::FragmentSection::new(Some("section 1".to_string()), vec![]);
let section2 = semantic::FragmentSection::new(Some("section 2".to_string()), vec![]);
let fragment =
semantic::Fragment::new("alt".to_string(), vec![section1, section2], fragment_def);
let start_y = 100.0;
let mut fragment_timing = FragmentTiming::new(&fragment, start_y);
fragment_timing.start_section(&fragment.sections()[0], 120.0);
let result = fragment_timing.end_section(180.0);
assert!(result.is_ok());
fragment_timing.start_section(&fragment.sections()[1], 180.0);
let result = fragment_timing.end_section(240.0);
assert!(result.is_ok());
fragment_timing.update_x(50.0, 200.0);
let end_y = 250.0;
let final_fragment = fragment_timing.into_fragment(end_y);
assert!(final_fragment.inner().size().height() > 0.0);
assert!(final_fragment.inner().size().width() > 0.0);
}
#[test]
fn test_fragment_timing_bounds_tracking() {
let fragment_def = Rc::new(draw::FragmentDefinition::default());
let fragment = semantic::Fragment::new("opt".to_string(), vec![], fragment_def);
let mut fragment_timing = FragmentTiming::new(&fragment, 100.0);
assert_eq!(fragment_timing.min_x, f32::MAX);
assert_eq!(fragment_timing.max_x, f32::MIN);
fragment_timing.update_x(50.0, 150.0);
assert_eq!(fragment_timing.min_x, 50.0);
assert_eq!(fragment_timing.max_x, 150.0);
fragment_timing.update_x(30.0, 100.0);
assert_eq!(fragment_timing.min_x, 30.0);
assert_eq!(fragment_timing.max_x, 150.0);
fragment_timing.update_x(60.0, 200.0);
assert_eq!(fragment_timing.min_x, 30.0); assert_eq!(fragment_timing.max_x, 200.0);
fragment_timing.update_x(40.0, 180.0);
assert_eq!(fragment_timing.min_x, 30.0);
assert_eq!(fragment_timing.max_x, 200.0);
}
#[test]
fn test_section_header_height_first_section_header_dominates() {
let fragment_def = Rc::new(draw::FragmentDefinition::default());
let section = semantic::FragmentSection::new(Some("x".to_string()), vec![]);
let fragment =
semantic::Fragment::new("alt".to_string(), vec![section], fragment_def.clone());
let fragment_timing = FragmentTiming::new(&fragment, 0.0);
let height = fragment_timing.section_header_height(&fragment.sections()[0]);
let header_height = fragment_def.header_size("alt").height();
assert_eq!(height, header_height);
}
#[test]
fn test_section_header_height_first_section_title_dominates() {
let fragment_def = Rc::new(draw::FragmentDefinition::default());
let tall_title = "line1\nline2\nline3\nline4\nline5";
let section = semantic::FragmentSection::new(Some(tall_title.to_string()), vec![]);
let fragment =
semantic::Fragment::new("alt".to_string(), vec![section], fragment_def.clone());
let fragment_timing = FragmentTiming::new(&fragment, 0.0);
let height = fragment_timing.section_header_height(&fragment.sections()[0]);
let title_height = fragment_def.section_header_size(Some(tall_title)).height();
assert_eq!(height, title_height);
}
#[test]
fn test_section_header_height_subsequent_section() {
let fragment_def = Rc::new(draw::FragmentDefinition::default());
let section1 = semantic::FragmentSection::new(Some("guard1".to_string()), vec![]);
let section2 = semantic::FragmentSection::new(Some("guard2".to_string()), vec![]);
let fragment = semantic::Fragment::new(
"alt".to_string(),
vec![section1, section2],
fragment_def.clone(),
);
let mut fragment_timing = FragmentTiming::new(&fragment, 0.0);
fragment_timing.start_section(&fragment.sections()[0], 0.0);
fragment_timing.end_section(50.0).unwrap();
let height = fragment_timing.section_header_height(&fragment.sections()[1]);
let title_height = fragment_def.section_header_size(Some("guard2")).height();
assert_eq!(height, title_height);
}
#[test]
fn test_section_header_height_no_title() {
let fragment_def = Rc::new(draw::FragmentDefinition::default());
let section = semantic::FragmentSection::new(None, vec![]);
let fragment =
semantic::Fragment::new("opt".to_string(), vec![section], fragment_def.clone());
let mut fragment_timing = FragmentTiming::new(&fragment, 0.0);
fragment_timing.start_section(&fragment.sections()[0], 0.0);
fragment_timing.end_section(50.0).unwrap();
let no_title_section = semantic::FragmentSection::new(None, vec![]);
let height = fragment_timing.section_header_height(&no_title_section);
assert_eq!(height, 0.0);
}
#[test]
fn test_bottom_padding_delegates_to_definition() {
let mut fragment_def = draw::FragmentDefinition::default();
fragment_def.set_bounds_padding(orrery_core::geometry::Insets::new(5.0, 10.0, 15.0, 20.0));
let fragment_def = Rc::new(fragment_def);
let fragment = semantic::Fragment::new("loop".to_string(), vec![], fragment_def);
let fragment_timing = FragmentTiming::new(&fragment, 0.0);
assert_eq!(fragment_timing.bottom_padding(), 15.0);
}
}