use std::collections::{HashMap, HashSet};
use crate::preview::mermaid::flowchart::Flowchart;
use crate::preview::mermaid::layout::Point;
use super::shapes::Size;
use super::SpecBlock;
pub const TITLE_PAD_Y: f64 = 2.0;
pub const TITLE_PAD_X: f64 = super::shapes::PADDING;
pub const STROKE_WIDTH: f64 = 1.0;
pub const CORNER_RADIUS: f64 = super::shapes::CORNER_RADIUS;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cluster {
pub id: String,
pub title: String,
pub parent: Option<String>,
pub depth: usize,
pub member_nodes: Vec<String>,
pub child_clusters: Vec<String>,
pub dashed: bool,
}
#[derive(Debug, Clone, Default)]
pub struct Tree {
clusters: Vec<Cluster>,
index: HashMap<String, usize>,
}
impl Tree {
pub fn build(chart: &Flowchart, is_node: impl Fn(&str) -> bool) -> Tree {
let blocks: Vec<SpecBlock> = chart
.subgraphs
.iter()
.map(|s| SpecBlock {
id: s.id.clone(),
title: s.title.clone(),
members: s.members.clone(),
dashed: false,
})
.collect();
Tree::from_blocks(&blocks, is_node)
}
pub fn from_blocks(blocks: &[SpecBlock], is_node: impl Fn(&str) -> bool) -> Tree {
let ids: HashSet<&str> = blocks.iter().map(|s| s.id.as_str()).collect();
let mut parent_of: HashMap<&str, &str> = HashMap::new();
for s in blocks {
for m in &s.members {
if ids.contains(m.as_str()) {
parent_of.insert(m.as_str(), s.id.as_str());
}
}
}
let by_id: HashMap<&str, &SpecBlock> = blocks.iter().map(|s| (s.id.as_str(), s)).collect();
let mut holds: HashMap<&str, bool> = HashMap::new();
for s in blocks {
holds_a_node(s, &by_id, &is_node, &mut holds, 0);
}
let mut clusters: Vec<Cluster> = Vec::new();
for s in blocks {
if !holds.get(s.id.as_str()).copied().unwrap_or(false) {
continue;
}
let parent = parent_of.get(s.id.as_str()).map(|p| p.to_string());
let mut member_nodes = Vec::new();
let mut child_clusters = Vec::new();
for m in &s.members {
if ids.contains(m.as_str()) {
if holds.get(m.as_str()).copied().unwrap_or(false) {
child_clusters.push(m.clone());
}
} else if is_node(m) {
member_nodes.push(m.clone());
}
}
clusters.push(Cluster {
id: s.id.clone(),
title: s.title.clone(),
parent,
depth: 0,
member_nodes,
child_clusters,
dashed: s.dashed,
});
}
let mut index: HashMap<String, usize> = HashMap::new();
for (i, c) in clusters.iter().enumerate() {
index.insert(c.id.clone(), i);
}
for c in &mut clusters {
if let Some(p) = &c.parent {
if !index.contains_key(p) {
c.parent = None;
}
}
}
let mut tree = Tree { clusters, index };
for i in 0..tree.clusters.len() {
tree.clusters[i].depth = tree.depth_of(i);
}
tree
}
fn depth_of(&self, i: usize) -> usize {
let mut depth = 0;
let mut cur = self.clusters[i].parent.clone();
while let Some(p) = cur {
depth += 1;
if depth > self.clusters.len() {
return depth;
}
cur = self
.index
.get(&p)
.and_then(|j| self.clusters[*j].parent.clone());
}
depth
}
pub fn is_empty(&self) -> bool {
self.clusters.is_empty()
}
pub fn iter(&self) -> std::slice::Iter<'_, Cluster> {
self.clusters.iter()
}
pub fn get(&self, id: &str) -> Option<&Cluster> {
self.index.get(id).map(|i| &self.clusters[*i])
}
pub fn contains(&self, id: &str) -> bool {
self.index.contains_key(id)
}
pub fn anchor<'a>(&'a self, id: &'a str, is_node: &dyn Fn(&str) -> bool) -> Option<&'a str> {
if is_node(id) {
return Some(id);
}
let c = self.get(id)?;
if let Some(n) = c.member_nodes.first() {
return Some(n.as_str());
}
for child in &c.child_clusters {
if let Some(n) = self.anchor(child, is_node) {
return Some(n);
}
}
None
}
pub fn touches(&self, id: &str, cluster_id: &str) -> bool {
if id == cluster_id {
return true;
}
let mut owner: Option<&str> = None;
for c in &self.clusters {
if c.member_nodes.iter().any(|m| m == id) {
owner = Some(c.id.as_str());
break;
}
}
let start = match owner {
Some(o) => o,
None if self.contains(id) => id,
None => return false,
};
let mut cur = Some(start);
let mut hops = 0;
while let Some(v) = cur {
if v == cluster_id {
return true;
}
hops += 1;
if hops > self.clusters.len() + 1 {
return false;
}
cur = self.get(v).and_then(|c| c.parent.as_deref());
}
false
}
}
fn holds_a_node<'a>(
s: &'a SpecBlock,
by_id: &HashMap<&'a str, &'a SpecBlock>,
is_node: &impl Fn(&str) -> bool,
memo: &mut HashMap<&'a str, bool>,
depth: usize,
) -> bool {
if let Some(v) = memo.get(s.id.as_str()) {
return *v;
}
if depth > by_id.len() {
return false;
}
let mut found = false;
for m in &s.members {
if let Some(child) = by_id.get(m.as_str()) {
if holds_a_node(child, by_id, is_node, memo, depth + 1) {
found = true;
}
} else if is_node(m) {
found = true;
}
}
memo.insert(s.id.as_str(), found);
found
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub left: f64,
pub top: f64,
pub right: f64,
pub bottom: f64,
}
impl Rect {
pub fn new(center: &Point, size: Size) -> Rect {
Rect {
left: center.x - size.w / 2.0,
top: center.y - size.h / 2.0,
right: center.x + size.w / 2.0,
bottom: center.y + size.h / 2.0,
}
}
pub fn center(&self) -> Point {
Point::new(
(self.left + self.right) / 2.0,
(self.top + self.bottom) / 2.0,
)
}
pub fn size(&self) -> Size {
Size::new(self.right - self.left, self.bottom - self.top)
}
pub fn absorb(&mut self, other: &Rect) {
self.left = self.left.min(other.left);
self.top = self.top.min(other.top);
self.right = self.right.max(other.right);
self.bottom = self.bottom.max(other.bottom);
}
pub fn contains(&self, p: &Point) -> bool {
p.x > self.left && p.x < self.right && p.y > self.top && p.y < self.bottom
}
}
pub fn exit_point(a: &Point, b: &Point, rect: &Rect) -> Point {
let (dx, dy) = (b.x - a.x, b.y - a.y);
let mut best: Option<f64> = None;
let mut consider = |t: f64, on_axis: bool| {
if (0.0..=1.0).contains(&t) && on_axis && best.is_none_or(|cur| t < cur) {
best = Some(t);
}
};
const EPS: f64 = 1e-9;
if dx.abs() > 0.0 {
for x in [rect.left, rect.right] {
let t = (x - a.x) / dx;
let y = a.y + t * dy;
consider(t, y >= rect.top - EPS && y <= rect.bottom + EPS);
}
}
if dy.abs() > 0.0 {
for y in [rect.top, rect.bottom] {
let t = (y - a.y) / dy;
let x = a.x + t * dx;
consider(t, x >= rect.left - EPS && x <= rect.right + EPS);
}
}
match best {
Some(t) => Point::new(a.x + t * dx, a.y + t * dy),
None => b.clone(),
}
}
pub fn cut_start(points: &[Point], rect: &Rect) -> Vec<Point> {
if points.is_empty() {
return Vec::new();
}
let first_outside = points.iter().position(|p| !rect.contains(p));
match first_outside {
Some(0) => points.to_vec(),
Some(i) => {
let mut out = Vec::with_capacity(points.len() - i + 1);
out.push(exit_point(&points[i - 1], &points[i], rect));
out.extend_from_slice(&points[i..]);
out
}
None => {
let center = rect.center();
let far = points.last().cloned().unwrap_or_else(|| center.clone());
vec![exit_point(¢er, &push_out(&far, rect), rect)]
}
}
}
pub fn cut_end(points: &[Point], rect: &Rect) -> Vec<Point> {
let reversed: Vec<Point> = points.iter().rev().cloned().collect();
let mut out = cut_start(&reversed, rect);
out.reverse();
out
}
fn push_out(p: &Point, rect: &Rect) -> Point {
let c = rect.center();
let (dx, dy) = (p.x - c.x, p.y - c.y);
let len = dx.hypot(dy);
let span = rect.size().w + rect.size().h + 1.0;
if len < 1e-9 {
return Point::new(c.x + span, c.y);
}
Point::new(c.x + dx / len * span, c.y + dy / len * span)
}