use svg::node::element::Text;
use svg::node::element::{Circle, Path, Definitions, Marker, Title, Description};
use svg::node::Text as TextNode;
use svg::Document;
use crate::intent::{RenderIntentGraph, CurveType};
use crate::graph::ShapeHint;
use crate::layout::profile::{RenderProfile, ArrowStyle};
use svg::Node;
pub fn contact_point(
from: (f64, f64),
to: (f64, f64),
shape: ShapeHint,
center: (f64, f64),
size: f64,
) -> (f64, f64) {
let (cx, cy) = center;
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
let len = (dx * dx + dy * dy).sqrt().max(1e-6);
let (ux, uy) = (dx / len, dy / len);
match shape {
ShapeHint::Circle => (cx - ux * size, cy - uy * size),
ShapeHint::Box => {
let half = size;
let scale = 1.0 / (ux.abs().max(uy.abs()));
(cx - ux * scale * half, cy - uy * scale * half)
}
ShapeHint::Diamond => {
let half = size;
let diag_len = (2.0f64).sqrt();
let scale = 1.0 / ((ux.abs() + uy.abs()) / diag_len);
(cx - ux * scale * half, cy - uy * scale * half)
}
}
}
pub fn to_svg(intent: &RenderIntentGraph, profile: &RenderProfile) -> Document {
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
let mut max_x = f64::MIN;
let mut max_y = f64::MIN;
for node in &intent.nodes {
let (x, y) = node.center;
let r = node.radius;
min_x = min_x.min(x - r);
max_x = max_x.max(x + r);
min_y = min_y.min(y - r);
max_y = max_y.max(y + r);
}
let padding = 10.0;
let view_x = min_x - padding;
let view_y = min_y - padding;
let view_w = (max_x - min_x) + 2.0 * padding;
let view_h = (max_y - min_y) + 2.0 * padding;
let mut doc = Document::new()
.set("viewBox", (view_x, view_y, view_w, view_h))
.set("xmlns", "http://www.w3.org/2000/svg");
if let Some(title) = &profile.title {
doc = doc.add(Title::new().add(TextNode::new(title.clone())));
}
if let Some(tool_name) = &profile.tool {
doc = doc.add(Description::new().add(TextNode::new(tool_name.clone())));
}
let mut defs = Definitions::new();
if !profile.no_arrows {
let mut arrow = Marker::new()
.set("id", "arrow")
.set("viewBox", "0 0 10 10")
.set("refX", 10)
.set("refY", 5)
.set("markerWidth", profile.arrow_size)
.set("markerHeight", profile.arrow_size)
.set("orient", "auto-start-reverse");
match profile.arrow_style {
ArrowStyle::Triangle => {
arrow.append(
Path::new()
.set("d", "M 0 0 L 10 5 L 0 10 z")
.set("fill", "black"),
);
}
ArrowStyle::Dot => {
arrow = Marker::new()
.set("id", "arrow")
.set("viewBox", "0 0 10 10")
.set("refX", profile.arrow_size / 2.0)
.set("refY", profile.arrow_size / 2.0)
.set("markerWidth", profile.arrow_size)
.set("markerHeight", profile.arrow_size)
.set("orient", "auto-start-reverse");
arrow.append(
Circle::new()
.set("cx", 0)
.set("cy", 0)
.set("r", profile.arrow_size / 2.0)
.set("fill", "black"),
);
}
}
defs = defs.add(arrow);
}
doc = doc.add(defs);
for node in &intent.nodes {
let base = Circle::new()
.set("cx", node.center.0)
.set("cy", node.center.1)
.set("r", node.radius)
.set("stroke", node.stroke_color.clone())
.set("stroke-width", node.stroke_width)
.set("fill", node.fill_color.clone());
doc = doc.add(base);
if node.is_accept {
let outer = Circle::new()
.set("cx", node.center.0)
.set("cy", node.center.1)
.set("r", node.radius + (node.stroke_width * 2.0))
.set("stroke", node.stroke_color.clone())
.set("stroke-width", node.stroke_width)
.set("fill", "none");
doc = doc.add(outer);
}
if node.is_start {
let arrow_len = 30.0;
let extra = if node.is_accept { node.stroke_width * 2.0 } else { 0.0 };
let x_end = node.center.0 - node.radius - extra;
let x_start = x_end - arrow_len;
let y = node.center.1;
let arrow_path = Path::new()
.set("d", format!("M {} {} L {} {}", x_start, y, x_end, y))
.set("stroke", node.stroke_color.clone())
.set("stroke-width", profile.stroke_width)
.set("fill", "none")
.set("marker-end", "url(#arrow)");
doc = doc.add(arrow_path);
}
let label = Text::new()
.set("x", node.center.0 + node.label_offset.0)
.set("y", node.center.1 + node.label_offset.1)
.set("font-size", node.font_size)
.set("font-family", "monospace")
.set("fill", node.font_color.clone())
.set("stroke", "white")
.set("stroke-width", node.stroke_width)
.set("paint-order", "stroke fill")
.set("text-anchor", "middle")
.add(TextNode::new(node.label.clone()));
doc = doc.add(label);
}
for edge in &intent.edges {
println!("EDGE {} -> {} has curve: {:?}", edge.from, edge.to, edge.curve);
let from_node = &intent.nodes[edge.from];
let to_node = &intent.nodes[edge.to];
let from_r = if from_node.is_accept {
from_node.radius + (from_node.stroke_width * 2.0)
} else {
from_node.radius
};
let to_r = if to_node.is_accept {
to_node.radius + (to_node.stroke_width * 2.0)
} else {
to_node.radius
};
let (x1, y1) = contact_point(
to_node.center,
from_node.center,
from_node.shape,
from_node.center,
from_r,
);
let (x2, y2) = contact_point(
from_node.center,
to_node.center,
to_node.shape,
to_node.center,
to_r,
);
let dx = x2 - x1;
let dy = y2 - y1;
let len = (dx * dx + dy * dy).sqrt().max(1.0);
let (nx, ny) = (dx / len, dy / len);
let (px, py) = (-ny, nx);
let cx = (x1 + x2) / 2.0 + px * (edge.bend_offset as f64) * 40.0;
let cy = (y1 + y2) / 2.0 + py * (edge.bend_offset as f64) * 40.0;
let data = match &edge.curve {
CurveType::Straight => format!("M {} {} L {} {}", x1, y1, x2, y2),
CurveType::Cubic { control1, control2 } => {
format!(
"M {} {} C {} {}, {} {}, {} {}",
x1, y1,
control1.0, control1.1,
control2.0, control2.1,
x2, y2
)
}
CurveType::Loop { .. } => {
let (cxn, cyn) = from_node.center;
let node_r = from_node.radius as f64;
let dir = (profile.loop_angle as f64).to_radians();
let half_span = (45.0_f64).to_radians();
let a1 = dir - half_span;
let a2 = dir + half_span;
let x1s = cxn + a1.cos() * node_r;
let y1s = cyn + a1.sin() * node_r;
let x2s = cxn + a2.cos() * node_r;
let y2s = cyn + a2.sin() * node_r;
let ext = profile.loop_radius as f64 * 2.0;
let cx1 = x1s + dir.cos() * ext;
let cy1 = y1s + dir.sin() * ext;
let cx2 = x2s + dir.cos() * ext;
let cy2 = y2s + dir.sin() * ext;
format!(
"M {x1s:.2} {y1s:.2} C {cx1:.2} {cy1:.2}, {cx2:.2} {cy2:.2}, {x2s:.2} {y2s:.2}",
x1s = x1s, y1s = y1s,
cx1 = cx1, cy1 = cy1,
cx2 = cx2, cy2 = cy2,
x2s = x2s, y2s = y2s
)
}
};
let mut path_elem = Path::new()
.set("d", data)
.set("stroke", edge.stroke_color.as_str())
.set("fill", "none")
.set("stroke-width", profile.stroke_width);
if !profile.no_arrows {
path_elem = path_elem.set("marker-end", "url(#arrow)");
}
let label = Text::new()
.set("x", edge.label_pos.0)
.set("y", edge.label_pos.1)
.set("font-size", edge.label_font_size)
.set("font-family", "monospace")
.set("fill", edge.label_font_color.as_str())
.set("stroke", "white")
.set("stroke-width", 3)
.set("paint-order", "stroke fill")
.set("text-anchor", "middle")
.add(TextNode::new(&edge.label));
doc = doc.add(path_elem).add(label);
if profile.show_debug_overlay {
let debug_label = Text::new()
.set("x", cx)
.set("y", cy - 10.0)
.set("font-size", 8)
.set("font-family", "monospace")
.set("fill", "red")
.set("text-anchor", "middle")
.add(TextNode::new(format!("{:.2}", edge.bend_offset)));
doc = doc.add(debug_label);
}
}
doc
}