use super::*;
use crate::generated::state_text_overrides_11_12_2 as state_text_overrides;
#[derive(Debug, Clone, Copy)]
struct StateEdgeBoundaryNode {
x: f64,
y: f64,
width: f64,
height: f64,
}
fn state_edge_dedup_consecutive_points(
input: &[crate::model::LayoutPoint],
) -> Vec<crate::model::LayoutPoint> {
if input.len() <= 1 {
return input.to_vec();
}
const EPS: f64 = 1e-9;
let mut out: Vec<crate::model::LayoutPoint> = Vec::with_capacity(input.len());
for p in input {
if out
.last()
.is_some_and(|prev| (prev.x - p.x).abs() <= EPS && (prev.y - p.y).abs() <= EPS)
{
continue;
}
out.push(p.clone());
}
out
}
fn state_edge_outside_node(
node: &StateEdgeBoundaryNode,
point: &crate::model::LayoutPoint,
) -> bool {
let dx = (point.x - node.x).abs();
let dy = (point.y - node.y).abs();
let w = node.width / 2.0;
let h = node.height / 2.0;
dx >= w || dy >= h
}
fn state_edge_rect_intersection(
node: &StateEdgeBoundaryNode,
inside_point: &crate::model::LayoutPoint,
outside_point: &crate::model::LayoutPoint,
) -> crate::model::LayoutPoint {
let x = node.x;
let y = node.y;
let w = node.width / 2.0;
let h = node.height / 2.0;
let q_abs = (outside_point.y - inside_point.y).abs();
let r_abs = (outside_point.x - inside_point.x).abs();
if (y - outside_point.y).abs() * w > (x - outside_point.x).abs() * h {
let q = if inside_point.y < outside_point.y {
outside_point.y - h - y
} else {
y - h - outside_point.y
};
let r = if q_abs == 0.0 {
0.0
} else {
(r_abs * q) / q_abs
};
let mut res = crate::model::LayoutPoint {
x: if inside_point.x < outside_point.x {
inside_point.x + r
} else {
inside_point.x - r_abs + r
},
y: if inside_point.y < outside_point.y {
inside_point.y + q_abs - q
} else {
inside_point.y - q_abs + q
},
};
if r.abs() <= 1e-9 {
res.x = outside_point.x;
res.y = outside_point.y;
}
if r_abs == 0.0 {
res.x = outside_point.x;
}
if q_abs == 0.0 {
res.y = outside_point.y;
}
return res;
}
let r = if inside_point.x < outside_point.x {
outside_point.x - w - x
} else {
x - w - outside_point.x
};
let q = if r_abs == 0.0 {
0.0
} else {
(q_abs * r) / r_abs
};
let mut ix = if inside_point.x < outside_point.x {
inside_point.x + r_abs - r
} else {
inside_point.x - r_abs + r
};
let mut iy = if inside_point.y < outside_point.y {
inside_point.y + q
} else {
inside_point.y - q
};
if r.abs() <= 1e-9 {
ix = outside_point.x;
iy = outside_point.y;
}
if r_abs == 0.0 {
ix = outside_point.x;
}
if q_abs == 0.0 {
iy = outside_point.y;
}
crate::model::LayoutPoint { x: ix, y: iy }
}
fn state_edge_cut_path_at_intersect(
input: &[crate::model::LayoutPoint],
boundary: &StateEdgeBoundaryNode,
) -> Vec<crate::model::LayoutPoint> {
if input.is_empty() {
return Vec::new();
}
let mut out: Vec<crate::model::LayoutPoint> = Vec::new();
let mut last_point_outside = input[0].clone();
let mut is_inside = false;
const EPS: f64 = 1e-9;
for point in input {
if !state_edge_outside_node(boundary, point) && !is_inside {
let inter = state_edge_rect_intersection(boundary, point, &last_point_outside);
if !out
.iter()
.any(|p| (p.x - inter.x).abs() <= EPS && (p.y - inter.y).abs() <= EPS)
{
out.push(inter);
}
is_inside = true;
} else {
last_point_outside = point.clone();
if !is_inside {
out.push(point.clone());
}
}
}
out
}
fn state_edge_boundary_for_cluster(
ctx: &StateRenderCtx<'_>,
cluster_id: &str,
ox: f64,
oy: f64,
) -> Option<StateEdgeBoundaryNode> {
let mut resolved = cluster_id;
if !ctx.layout_clusters_by_id.contains_key(resolved) {
if let Some(rest) = resolved.strip_prefix("state-") {
if let Some((base, suffix)) = rest.rsplit_once('-') {
if !base.is_empty()
&& !suffix.is_empty()
&& suffix.bytes().all(|b| b.is_ascii_digit())
{
resolved = base;
}
}
}
}
let n = ctx.layout_clusters_by_id.get(resolved).copied()?;
Some(StateEdgeBoundaryNode {
x: n.x - ox,
y: n.y - oy,
width: n.width,
height: n.height,
})
}
fn state_edge_prepare_points(
ctx: &StateRenderCtx<'_>,
le: &crate::model::LayoutEdge,
edge_id: &str,
origin_x: f64,
origin_y: f64,
) -> (
Vec<crate::model::LayoutPoint>,
Vec<crate::model::LayoutPoint>,
) {
let mut local_points: Vec<crate::model::LayoutPoint> = Vec::new();
for p in &le.points {
local_points.push(crate::model::LayoutPoint {
x: p.x - origin_x,
y: p.y - origin_y,
});
}
let is_cyclic_special = edge_id.contains("-cyclic-special-");
let mut points_for_curve = if is_cyclic_special {
state_edge_dedup_consecutive_points(&local_points)
} else {
local_points.clone()
};
if let Some(tc) = le.to_cluster.as_deref() {
if let Some(boundary) = state_edge_boundary_for_cluster(ctx, tc, origin_x, origin_y) {
points_for_curve = state_edge_cut_path_at_intersect(&points_for_curve, &boundary);
}
}
if let Some(fc) = le.from_cluster.as_deref() {
if let Some(boundary) = state_edge_boundary_for_cluster(ctx, fc, origin_x, origin_y) {
let mut rev = points_for_curve;
rev.reverse();
rev = state_edge_cut_path_at_intersect(&rev, &boundary);
rev.reverse();
points_for_curve = rev;
}
}
if is_cyclic_special {
if edge_id.contains("-cyclic-special-mid") && points_for_curve.len() > 3 {
points_for_curve = vec![
points_for_curve[0].clone(),
points_for_curve[points_for_curve.len() / 2].clone(),
points_for_curve[points_for_curve.len() - 1].clone(),
];
}
if points_for_curve.len() == 4 {
points_for_curve.remove(1);
}
if edge_id.ends_with("-cyclic-special-2") && points_for_curve.len() == 6 {
points_for_curve.remove(1);
}
}
(local_points, points_for_curve)
}
fn state_edge_encode_path(
ctx: &StateRenderCtx<'_>,
le: &crate::model::LayoutEdge,
edge_id: &str,
origin_x: f64,
origin_y: f64,
) -> (String, String) {
let (local_points, points_for_curve) =
state_edge_prepare_points(ctx, le, edge_id, origin_x, origin_y);
let data_points = base64::engine::general_purpose::STANDARD
.encode(serde_json::to_vec(&local_points).unwrap_or_default());
let d = curve_basis_path_d(&points_for_curve);
(d, data_points)
}
pub(super) fn render_state_edge_path(
out: &mut String,
ctx: &StateRenderCtx<'_>,
edge: &StateSvgEdge,
origin_x: f64,
origin_y: f64,
) {
let mut classes = "edge-thickness-normal edge-pattern-solid".to_string();
for c in edge.classes.split_whitespace() {
if c.trim().is_empty() {
continue;
}
classes.push(' ');
classes.push_str(c.trim());
}
let marker_end = if edge.arrow_type_end.trim() == "arrow_barb" {
Some(format!("url(#{}_stateDiagram-barbEnd)", ctx.diagram_id))
} else {
None
};
if edge.start == edge.end {
let start = edge.start.as_str();
let id1 = format!("{start}-cyclic-special-1");
let idm = format!("{start}-cyclic-special-mid");
let id2 = format!("{start}-cyclic-special-2");
let segments = [(&id1, None), (&idm, None), (&id2, marker_end.as_ref())];
for (sid, marker) in segments {
let Some(le) = ctx.layout_edges_by_id.get(sid.as_str()).copied() else {
continue;
};
if le.points.len() < 2 {
continue;
}
let (d, data_points) = state_edge_encode_path(ctx, le, sid, origin_x, origin_y);
let _ = write!(
out,
r#"<path d="{}" id="{}" class="{}" style="fill:none;;;fill:none" data-edge="true" data-et="edge" data-id="{}" data-points="{}""#,
d,
escape_xml_display(sid),
escape_xml_display(&classes),
escape_xml_display(sid),
data_points
);
if let Some(m) = marker {
let _ = write!(out, r#" marker-end="{}""#, escape_xml_display(m));
}
out.push_str("/>");
}
return;
}
let Some(le) = ctx.layout_edges_by_id.get(edge.id.as_str()).copied() else {
return;
};
if le.points.len() < 2 {
return;
}
let (d, data_points) = state_edge_encode_path(ctx, le, edge.id.as_str(), origin_x, origin_y);
let _ = write!(
out,
r#"<path d="{}" id="{}" class="{}" style="fill:none;;;fill:none" data-edge="true" data-et="edge" data-id="{}" data-points="{}""#,
d,
escape_xml_display(&edge.id),
escape_xml_display(&classes),
escape_xml_display(&edge.id),
data_points
);
if let Some(m) = marker_end {
let _ = write!(out, r#" marker-end="{}""#, escape_xml_display(&m));
}
out.push_str("/>");
}
pub(super) fn render_state_edge_label(
out: &mut String,
ctx: &StateRenderCtx<'_>,
edge: &StateSvgEdge,
origin_x: f64,
origin_y: f64,
) {
fn edge_label_div_style(label_w: f64) -> String {
let max_width = state_text_overrides::state_edge_label_max_width_px();
if label_w >= max_width - 1e-3 {
format!(
"display: table; white-space: break-spaces; line-height: 1.5; max-width: {}px; text-align: center; width: {}px;",
fmt_display(max_width),
fmt_display(max_width),
)
} else {
format!(
"display: table-cell; white-space: nowrap; line-height: 1.5; max-width: {}px; text-align: center;",
fmt_display(max_width),
)
}
}
fn mermaid_round_number(num: f64, precision: i32) -> f64 {
let factor = 10_f64.powi(precision);
(num * factor).round() / factor
}
fn mermaid_distance(
point: &crate::model::LayoutPoint,
prev: Option<&crate::model::LayoutPoint>,
) -> f64 {
let Some(prev) = prev else {
return 0.0;
};
((point.x - prev.x).powi(2) + (point.y - prev.y).powi(2)).sqrt()
}
fn mermaid_calculate_point(
points: &[crate::model::LayoutPoint],
distance_to_traverse: f64,
) -> Option<crate::model::LayoutPoint> {
let mut prev: Option<&crate::model::LayoutPoint> = None;
let mut remaining = distance_to_traverse;
for point in points {
if let Some(prev_point) = prev {
let vector_distance = mermaid_distance(point, Some(prev_point));
if vector_distance == 0.0 {
return Some(prev_point.clone());
}
if vector_distance < remaining {
remaining -= vector_distance;
} else {
let distance_ratio = remaining / vector_distance;
if distance_ratio <= 0.0 {
return Some(prev_point.clone());
}
if distance_ratio >= 1.0 {
return Some(point.clone());
}
if distance_ratio > 0.0 && distance_ratio < 1.0 {
return Some(crate::model::LayoutPoint {
x: mermaid_round_number(
(1.0 - distance_ratio) * prev_point.x + distance_ratio * point.x,
5,
),
y: mermaid_round_number(
(1.0 - distance_ratio) * prev_point.y + distance_ratio * point.y,
5,
),
});
}
}
}
prev = Some(point);
}
None
}
fn mermaid_calc_label_position(
points: &[crate::model::LayoutPoint],
) -> Option<crate::model::LayoutPoint> {
if points.is_empty() {
return None;
}
if points.len() == 1 {
return Some(points[0].clone());
}
let mut total_distance: f64 = 0.0;
let mut prev: Option<&crate::model::LayoutPoint> = None;
for point in points {
total_distance += mermaid_distance(point, prev);
prev = Some(point);
}
let remaining_distance = total_distance / 2.0;
mermaid_calculate_point(points, remaining_distance)
}
let empty_edge_label_style = edge_label_div_style(0.0);
let label_text = edge.label.trim();
if edge.start == edge.end {
let start = edge.start.as_str();
let id1 = format!("{start}-cyclic-special-1");
let idm = format!("{start}-cyclic-special-mid");
let id2 = format!("{start}-cyclic-special-2");
let _ = write!(
out,
r#"<g class="edgeLabel"><g class="label" data-id="{}" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="{}"><span class="edgeLabel"></span></div></foreignObject></g></g>"#,
escape_attr(&id1),
empty_edge_label_style.as_str()
);
if !label_text.is_empty() {
if let Some(le) = ctx.layout_edges_by_id.get(idm.as_str()).copied() {
if let Some(lbl) = le.label.as_ref() {
let cx = lbl.x - origin_x;
let cy = lbl.y - origin_y;
let w = lbl.width.max(0.0);
let h = lbl.height.max(0.0);
let _ = write!(
out,
r#"<g class="edgeLabel" transform="translate({}, {})"><g class="label" data-id="{}" transform="translate({}, {})"><foreignObject width="{}" height="{}"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="{}"><span class="edgeLabel">{}</span></div></foreignObject></g></g>"#,
fmt_display(cx),
fmt_display(cy),
escape_xml_display(&idm),
fmt_display(-w / 2.0),
fmt_display(-h / 2.0),
fmt_display(w),
fmt_display(h),
edge_label_div_style(w),
state_edge_label_html(label_text)
);
}
}
} else {
let _ = write!(
out,
r#"<g class="edgeLabel"><g class="label" data-id="{}" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="{}"><span class="edgeLabel"></span></div></foreignObject></g></g>"#,
escape_xml_display(&idm),
empty_edge_label_style.as_str()
);
}
let _ = write!(
out,
r#"<g class="edgeLabel"><g class="label" data-id="{}" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="{}"><span class="edgeLabel"></span></div></foreignObject></g></g>"#,
escape_attr(&id2),
empty_edge_label_style.as_str()
);
return;
}
if label_text.is_empty() {
let _ = write!(
out,
r#"<g class="edgeLabel"><g class="label" data-id="{}" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="{}"><span class="edgeLabel"></span></div></foreignObject></g></g>"#,
escape_xml_display(&edge.id),
empty_edge_label_style.as_str()
);
return;
}
let Some(le) = ctx.layout_edges_by_id.get(edge.id.as_str()).copied() else {
return;
};
let Some(lbl) = le.label.as_ref() else {
return;
};
let mut cx = lbl.x - origin_x;
let mut cy = lbl.y - origin_y;
let (_local_points, points_for_curve) =
state_edge_prepare_points(ctx, le, edge.id.as_str(), origin_x, origin_y);
fn mermaid_is_label_coordinate_in_path(
point: &crate::model::LayoutPoint,
d_attr: &str,
) -> bool {
let rounded_x = point.x.round() as i64;
let rounded_y = point.y.round() as i64;
let bytes = d_attr.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
let is_start = b.is_ascii_digit() || b == b'-' || b == b'.';
if !is_start {
i += 1;
continue;
}
let start = i;
i += 1;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_digit() || b == b'.' {
i += 1;
continue;
}
break;
}
let token = &d_attr[start..i];
if let Ok(v) = token.parse::<f64>() {
let r = v.round() as i64;
if r == rounded_x || r == rounded_y {
return true;
}
}
}
false
}
let mut points_has_changed = le.to_cluster.is_some() || le.from_cluster.is_some();
if !points_has_changed && !points_for_curve.is_empty() {
let d_attr = curve_basis_path_d(&points_for_curve);
let mid = &points_for_curve[points_for_curve.len() / 2];
if !mermaid_is_label_coordinate_in_path(mid, &d_attr) {
points_has_changed = true;
}
}
if points_has_changed {
if let Some(pos) = mermaid_calc_label_position(&points_for_curve) {
cx = pos.x;
cy = pos.y;
}
}
let w = lbl.width.max(0.0);
let h = lbl.height.max(0.0);
let _ = write!(
out,
r#"<g class="edgeLabel" transform="translate({}, {})"><g class="label" data-id="{}" transform="translate({}, {})"><foreignObject width="{}" height="{}"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="{}"><span class="edgeLabel">{}</span></div></foreignObject></g></g>"#,
fmt_display(cx),
fmt_display(cy),
escape_xml_display(&edge.id),
fmt_display(-w / 2.0),
fmt_display(-h / 2.0),
fmt_display(w),
fmt_display(h),
edge_label_div_style(w),
state_edge_label_html(label_text)
);
}