use std::{
collections::{HashMap, HashSet, VecDeque},
rc::Rc,
};
use orrery_core::{
draw::{Drawable, PositionedArrowWithText, Shape, ShapeWithText, Text},
geometry::{Insets, Point, Size},
identifier::Id,
semantic::{Block, Relation},
};
use crate::{
error::RenderError,
layout::{
component::{self, ArrowPlacer, Component, Layout, SmartArrowPlacer},
engines::{ComponentEngine, EmbeddedLayouts},
layer::{ContentStack, PositionedContent},
},
structure::{ComponentGraph, ContainmentScope},
};
#[derive(Default)]
pub struct Engine {
padding: Insets,
text_padding: f32,
min_spacing: f32,
arrow_placer: SmartArrowPlacer,
}
impl Engine {
pub fn new() -> Self {
Self {
text_padding: 20.0,
..Self::default()
}
}
pub fn set_padding(&mut self, padding: Insets) -> &mut Self {
self.padding = padding;
self
}
#[allow(dead_code)]
pub fn set_text_padding(&mut self, padding: f32) -> &mut Self {
self.text_padding = padding;
self
}
pub fn set_min_spacing(&mut self, spacing: f32) -> &mut Self {
self.min_spacing = spacing;
self
}
pub fn calculate_layout<'a>(
&self,
graph: &'a ComponentGraph<'a, '_>,
embedded_layouts: &EmbeddedLayouts<'a>,
) -> Result<ContentStack<Layout<'a>>, RenderError> {
let mut content_stack = ContentStack::<Layout<'a>>::new();
let mut positioned_content_sizes = HashMap::<Id, Size>::new();
for containment_scope in graph.containment_scopes() {
let mut component_shapes = self.calculate_component_shapes(
graph,
containment_scope,
&positioned_content_sizes,
embedded_layouts,
)?;
let positions = self.positions(graph, containment_scope, &component_shapes)?;
let components: Vec<Component> = graph
.scope_nodes(containment_scope)
.map(|node| {
let position = *positions.get(&node.id()).ok_or_else(|| {
RenderError::Layout(format!(
"Position not found for node '{node}' during component layout"
))
})?;
let shape_with_text = component_shapes.remove(&node.id()).ok_or_else(|| {
RenderError::Layout(format!(
"Shape not found for node '{node}' during component layout"
))
})?;
Ok(Component::new(node, shape_with_text, position))
})
.collect::<Result<Vec<_>, RenderError>>()?;
let component_indices: HashMap<_, _> = components
.iter()
.enumerate()
.map(|(idx, component)| (component.node_id(), idx))
.collect();
let relations = self.place_scope_relations(
graph,
containment_scope,
&components,
&component_indices,
);
let positioned_content = PositionedContent::new(Layout::new(components, relations));
if let Some(container) = containment_scope.container() {
let size = positioned_content.layout_size();
positioned_content_sizes.insert(container, size);
}
content_stack.push(positioned_content);
}
component::adjust_positioned_contents_offset(&mut content_stack, graph)?;
Ok(content_stack)
}
fn place_scope_relations<'a>(
&self,
graph: &'a ComponentGraph<'a, '_>,
containment_scope: &ContainmentScope,
components: &[Component<'a>],
component_indices: &HashMap<Id, usize>,
) -> Vec<PositionedArrowWithText<'a>> {
let mut buckets: HashMap<(Id, Id), Vec<&'a Relation>> = HashMap::new();
for relation in graph.scope_relations(containment_scope) {
let source_id = relation.source();
let target_id = relation.target();
if !component_indices.contains_key(&source_id)
|| !component_indices.contains_key(&target_id)
{
continue;
}
let key = if buckets.contains_key(&(target_id, source_id)) {
(target_id, source_id)
} else {
(source_id, target_id)
};
buckets.entry(key).or_default().push(relation);
}
buckets
.into_iter()
.flat_map(|((src_id, tgt_id), bucket)| {
let src = &components[component_indices[&src_id]];
let tgt = &components[component_indices[&tgt_id]];
self.arrow_placer.place(&bucket, src, tgt)
})
.collect()
}
fn calculate_component_shapes<'a>(
&self,
graph: &'a ComponentGraph<'a, '_>,
containment_scope: &ContainmentScope,
positioned_content_sizes: &HashMap<Id, Size>,
embedded_layouts: &EmbeddedLayouts<'a>,
) -> Result<HashMap<Id, ShapeWithText<'a>>, RenderError> {
let mut component_shapes: HashMap<Id, ShapeWithText<'a>> = HashMap::new();
for node in graph.scope_nodes(containment_scope) {
let mut shape = Shape::new(Rc::clone(node.shape_definition()));
shape.set_padding(self.padding);
let text = Text::new(node.shape_definition().text(), node.display_text());
let mut shape_with_text = ShapeWithText::new(shape, Some(text));
match node.block() {
Block::Diagram(_) => {
let layout = embedded_layouts.get(&node.id()).ok_or_else(|| {
RenderError::Layout(format!(
"Embedded layout not found for diagram block '{node}'"
))
})?;
let content_size = layout.calculate_size();
shape_with_text
.set_inner_content_size(content_size)
.map_err(|err| {
RenderError::Layout(format!(
"Failed to set content size for diagram block '{node}': {err}"
))
})?;
}
Block::Scope(_) => {
let content_size =
*positioned_content_sizes.get(&node.id()).ok_or_else(|| {
RenderError::Layout(format!("Scope size not found for node '{node}'"))
})?;
shape_with_text
.set_inner_content_size(content_size)
.map_err(|err| {
RenderError::Layout(format!(
"Failed to set content size for scope block '{node}': {err}"
))
})?;
}
Block::None => {
}
};
component_shapes.insert(node.id(), shape_with_text);
}
Ok(component_shapes)
}
fn positions<'a>(
&self,
graph: &'a ComponentGraph<'a, '_>,
containment_scope: &ContainmentScope,
component_shapes: &HashMap<Id, ShapeWithText<'a>>,
) -> Result<HashMap<Id, Point>, RenderError> {
let layers = Self::assign_layers_for_containment_scope_graph(graph, containment_scope)?;
let (layer_widths, layer_spacings) =
self.calculate_layer_metrics(graph, containment_scope, &layers, component_shapes)?;
let layer_x_positions = self.calculate_layer_x_positions(&layer_widths, &layer_spacings);
self.position_nodes_in_layers(&layers, &layer_x_positions, component_shapes)
}
fn calculate_layer_metrics<'a>(
&self,
graph: &'a ComponentGraph<'a, '_>,
containment_scope: &ContainmentScope,
layers: &[Vec<Id>],
component_shapes: &HashMap<Id, ShapeWithText<'a>>,
) -> Result<(Vec<f32>, Vec<f32>), RenderError> {
let layer_widths: Vec<f32> = layers
.iter()
.map(|layer| {
layer.iter().try_fold(0.0_f32, |max_width, &node_idx| {
let width = component_shapes
.get(&node_idx)
.map(|shape| shape.size().width())
.ok_or_else(|| {
RenderError::Layout(format!(
"Component shape not found for node '{}' during layer metrics calculation",
node_idx
))
})?;
Ok(max_width.max(width))
})
})
.collect::<Result<Vec<f32>, RenderError>>()?;
let mut layer_spacings =
vec![self.padding.horizontal_sum() / 2.0; layers.len().saturating_sub(1)];
for relation in graph.scope_relations(containment_scope) {
if let Some(text) = relation.text() {
let label_width = text.calculate_size().width();
let (source_layer, target_layer) = self.find_node_layers(relation, layers);
if let (Some(src), Some(tgt)) = (source_layer, target_layer)
&& src != tgt
{
let min_layer = src.min(tgt);
let needed_spacing = label_width + 30.0;
if min_layer < layer_spacings.len() {
layer_spacings[min_layer] = layer_spacings[min_layer].max(needed_spacing);
}
}
}
}
Ok((layer_widths, layer_spacings))
}
fn find_node_layers(
&self,
relation: &Relation,
layers: &[Vec<Id>],
) -> (Option<usize>, Option<usize>) {
let mut source_layer = None;
let mut target_layer = None;
for (layer_idx, layer_nodes) in layers.iter().enumerate() {
for node_id in layer_nodes {
if *node_id == relation.source() {
source_layer = Some(layer_idx);
}
if *node_id == relation.target() {
target_layer = Some(layer_idx);
}
}
}
(source_layer, target_layer)
}
fn calculate_layer_x_positions(
&self,
layer_widths: &[f32],
layer_spacings: &[f32],
) -> Vec<f32> {
let mut layer_x_positions = Vec::with_capacity(layer_widths.len());
let mut x_pos = 0.0;
for (i, width) in layer_widths.iter().enumerate() {
layer_x_positions.push(x_pos + width / 2.0);
let spacing = if i < layer_spacings.len() {
layer_spacings[i]
} else {
self.padding.horizontal_sum() / 2.0
};
x_pos += width + spacing;
}
layer_x_positions
}
fn position_nodes_in_layers<'a>(
&self,
layers: &[Vec<Id>],
layer_x_positions: &[f32],
component_shapes: &HashMap<Id, ShapeWithText<'a>>,
) -> Result<HashMap<Id, Point>, RenderError> {
let mut positions = HashMap::new();
for (layer_idx, layer_nodes) in layers.iter().enumerate() {
let x = layer_x_positions[layer_idx];
let mut y_pos = 0.0;
for (j, &node_idx) in layer_nodes.iter().enumerate() {
let node_height = component_shapes
.get(&node_idx)
.ok_or_else(|| {
RenderError::Layout(format!(
"Component shape not found for node '{}' during position calculation",
node_idx
))
})?
.size()
.height();
if j > 0 {
y_pos += self.padding.vertical_sum() / 2.0; }
let y = y_pos + node_height / 2.0;
positions.insert(node_idx, Point::new(x, y));
y_pos += node_height;
}
}
Ok(positions)
}
fn assign_layers_for_containment_scope_graph(
graph: &ComponentGraph,
containment_scope: &ContainmentScope,
) -> Result<Vec<Vec<Id>>, RenderError> {
let mut layers = Vec::new();
let mut visited = HashSet::new();
let root_nodes: Vec<_> = graph.scope_roots(containment_scope).collect();
let start_nodes = if root_nodes.is_empty() {
graph.scope_nodes(containment_scope).take(1).collect()
} else {
root_nodes
};
let mut queue = VecDeque::new();
for node in start_nodes {
queue.push_back((node, 0));
}
while let Some((node, layer)) = queue.pop_front() {
if !visited.insert(node.id()) {
continue;
}
while layers.len() <= layer {
layers.push(Vec::new());
}
layers[layer].push(node.id());
for neighbor in graph
.scope_outgoing_neighbors(containment_scope, node.id())
.filter(|node| !visited.contains(&node.id()))
{
queue.push_back((neighbor, layer + 1));
}
}
while visited.len() < containment_scope.nodes_count() {
let unvisited_node_id = containment_scope
.node_ids()
.find(|id| !visited.contains(id))
.ok_or_else(|| {
RenderError::Layout("Expected unvisited nodes but none found".to_string())
})?;
let unvisited_node = graph.node_by_id(unvisited_node_id).ok_or_else(|| {
RenderError::Layout(format!(
"Node '{}' from containment scope not found in graph",
unvisited_node_id
))
})?;
queue.push_back((unvisited_node, 0));
while let Some((node, layer)) = queue.pop_front() {
if !visited.insert(node.id()) {
continue;
}
while layers.len() <= layer {
layers.push(Vec::new());
}
layers[layer].push(node.id());
for neighbor in graph
.scope_outgoing_neighbors(containment_scope, node.id())
.filter(|node| !visited.contains(&node.id()))
{
queue.push_back((neighbor, layer + 1));
}
}
}
Ok(layers)
}
}
impl ComponentEngine for Engine {
fn calculate<'a>(
&self,
graph: &'a ComponentGraph<'a, '_>,
embedded_layouts: &EmbeddedLayouts<'a>,
) -> Result<ContentStack<Layout<'a>>, RenderError> {
self.calculate_layout(graph, embedded_layouts)
}
}