use crate::theme::{Palette, palette};
use drawlang_core::geom::*;
use drawlang_core::model::*;
use drawlang_core::text::FONT_FAMILY;
pub fn fmt_f(v: f64) -> String {
let r = (v * 100.0).round() / 100.0;
if r == r.trunc() {
format!("{}", r as i64)
} else {
let s = format!("{r:.2}");
s.trim_end_matches('0').trim_end_matches('.').to_string()
}
}
fn esc(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
pub fn render_svg(doc: &Document, g: &Geometry) -> String {
let p = palette(doc.canvas.theme);
let mut out = String::with_capacity(16 * 1024);
let (w, h) = (fmt_f(g.width), fmt_f(g.height));
out.push_str(&format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" height="{h}" font-family="{FONT_FAMILY}, Helvetica, Arial, sans-serif">"#
));
out.push('\n');
out.push_str(&format!(
r#"<rect width="{w}" height="{h}" fill="{}"/>"#,
p.bg
));
out.push('\n');
if let Some(t) = &g.title {
draw_label(&mut out, t, p.ink);
}
for id in doc.walk() {
if id == doc.root {
continue;
}
draw_element(&mut out, doc, g, id, p);
}
for route in &g.routes {
draw_edge(&mut out, doc, route, p);
}
for route in &g.routes {
if let Some(label) = &route.label {
draw_halo_label(&mut out, label, p);
}
}
draw_ports(&mut out, doc, g, p);
out.push_str("</svg>\n");
out
}
fn draw_element(out: &mut String, doc: &Document, g: &Geometry, id: ElementId, p: &Palette) {
let el = doc.el(id);
if el.kind.is_container() {
return; }
let r = g.rect(id);
let style = doc.resolved_style(id);
match el.kind {
ElementKind::Group => {
let stroke = p.resolve(style.color.as_ref(), p.group_border);
let sw = style.stroke.unwrap_or(1.2);
let corner = style.corner.unwrap_or(10.0);
let wash = p.resolve(style.fill.as_ref(), p.ink);
let wash_op = if style.fill.is_some() {
1.0
} else {
p.group_wash_opacity
};
out.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" rx="{}" fill="{}" fill-opacity="{}" stroke="{}" stroke-width="{}"{}/>"#,
fmt_f(r.x), fmt_f(r.y), fmt_f(r.w), fmt_f(r.h), fmt_f(corner),
wash, fmt_f(wash_op), stroke, fmt_f(sw),
dash_attr(&style),
));
out.push('\n');
if let Some(label) = g.labels.get(&id) {
let color = p.resolve(style.text_color.as_ref(), p.group_label);
draw_label(out, label, &color);
}
}
ElementKind::Node => {
let parent_is_node = el
.parent
.map(|q| doc.el(q).kind == ElementKind::Node)
.unwrap_or(false);
let default_fill = if parent_is_node { p.inner } else { p.surface };
let fill = p.resolve(style.fill.as_ref(), default_fill);
let stroke = p.resolve(style.color.as_ref(), p.node_border);
let sw = style.stroke.unwrap_or(1.4);
let shape = style.shape.unwrap_or(Shape::Rect);
match shape {
Shape::Rect | Shape::Pill => {
let corner = if shape == Shape::Pill {
r.h / 2.0
} else {
style.corner.unwrap_or(7.0)
};
out.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" rx="{}" fill="{}" stroke="{}" stroke-width="{}"{}/>"#,
fmt_f(r.x), fmt_f(r.y), fmt_f(r.w), fmt_f(r.h), fmt_f(corner),
fill, stroke, fmt_f(sw),
dash_attr(&style),
));
}
Shape::Ellipse => {
out.push_str(&format!(
r#"<ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{}" stroke="{}" stroke-width="{}"{}/>"#,
fmt_f(r.cx()), fmt_f(r.cy()), fmt_f(r.w / 2.0), fmt_f(r.h / 2.0),
fill, stroke, fmt_f(sw),
dash_attr(&style),
));
}
}
out.push('\n');
if let Some(label) = g.labels.get(&id) {
let color = match &style.text_color {
Some(c) => p.resolve(Some(c), p.ink),
None if style.fill.is_some() && luminance(&fill) < 0.45 => {
"#FAFBFC".to_string()
}
None if parent_is_node => p.edge_label.to_string(),
None => p.ink.to_string(),
};
draw_label(out, label, &color);
}
}
_ => {}
}
}
fn luminance(hex: &str) -> f64 {
let h = hex.trim_start_matches('#');
let (r, g, b) = match h.len() {
3 => (
u8::from_str_radix(&h[0..1].repeat(2), 16).unwrap_or(0),
u8::from_str_radix(&h[1..2].repeat(2), 16).unwrap_or(0),
u8::from_str_radix(&h[2..3].repeat(2), 16).unwrap_or(0),
),
6 | 8 => (
u8::from_str_radix(&h[0..2], 16).unwrap_or(0),
u8::from_str_radix(&h[2..4], 16).unwrap_or(0),
u8::from_str_radix(&h[4..6], 16).unwrap_or(0),
),
_ => return 1.0,
};
(0.2126 * r as f64 + 0.7152 * g as f64 + 0.0722 * b as f64) / 255.0
}
fn dash_attr(style: &Style) -> &'static str {
if style.dashed == Some(true) {
r#" stroke-dasharray="6 4""#
} else {
""
}
}
fn draw_label(out: &mut String, l: &LabelBlock, color: &str) {
let weight = if l.bold { r#" font-weight="bold""# } else { "" };
for (i, line) in l.lines.iter().enumerate() {
if line.is_empty() {
continue;
}
let y = l.y + l.baseline + l.line_height * i as f64;
let (x, anchor) = match l.align {
TextAlign::Center => (l.x + l.width / 2.0, r#" text-anchor="middle""#),
TextAlign::Left => (l.x, ""),
};
out.push_str(&format!(
r#"<text x="{}" y="{}" font-size="{}"{}{} fill="{}">{}</text>"#,
fmt_f(x),
fmt_f(y),
fmt_f(l.size),
weight,
anchor,
color,
esc(line)
));
out.push('\n');
}
}
fn draw_halo_label(out: &mut String, l: &LabelBlock, p: &Palette) {
let pad_x = 4.0;
let pad_y = 2.0;
out.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" rx="3" fill="{}" fill-opacity="0.92"/>"#,
fmt_f(l.x - pad_x),
fmt_f(l.y - pad_y),
fmt_f(l.width + 2.0 * pad_x),
fmt_f(l.height + 2.0 * pad_y),
p.bg,
));
out.push('\n');
draw_label(out, l, p.edge_label);
}
fn draw_edge(out: &mut String, doc: &Document, route: &EdgeRoute, p: &Palette) {
let edge = &doc.edges[route.edge];
let style = doc.edge_style(edge);
let color = p.resolve(style.color.as_ref(), p.edge);
let sw = style.stroke.unwrap_or(1.6);
let arrow_len = 7.0 + sw * 1.6;
let mut pts = route.points.clone();
let (end_apex, end_dir) = end_tangent(&pts, route.kind);
if route.arrow_end {
let trimmed = (
end_apex.0 - end_dir.0 * arrow_len * 0.8,
end_apex.1 - end_dir.1 * arrow_len * 0.8,
);
set_endpoint(&mut pts, route.kind, true, trimmed);
}
let (start_apex, start_dir) = start_tangent(&pts, route.kind);
if route.arrow_start {
let trimmed = (
start_apex.0 - start_dir.0 * arrow_len * 0.8,
start_apex.1 - start_dir.1 * arrow_len * 0.8,
);
set_endpoint(&mut pts, route.kind, false, trimmed);
}
let d = match route.kind {
RouteKind::Straight => {
format!(
"M {} {} L {} {}",
fmt_f(pts[0].0),
fmt_f(pts[0].1),
fmt_f(pts[1].0),
fmt_f(pts[1].1)
)
}
RouteKind::Cubic => format!(
"M {} {} C {} {}, {} {}, {} {}",
fmt_f(pts[0].0),
fmt_f(pts[0].1),
fmt_f(pts[1].0),
fmt_f(pts[1].1),
fmt_f(pts[2].0),
fmt_f(pts[2].1),
fmt_f(pts[3].0),
fmt_f(pts[3].1),
),
RouteKind::Ortho => ortho_path(&pts),
};
out.push_str(&format!(
r#"<path d="{d}" fill="none" stroke="{color}" stroke-width="{}" stroke-linecap="round" stroke-linejoin="round"{}/>"#,
fmt_f(sw),
dash_attr(&style),
));
out.push('\n');
if route.arrow_end {
draw_arrow(out, end_apex, end_dir, arrow_len, &color);
}
if route.arrow_start {
draw_arrow(out, start_apex, start_dir, arrow_len, &color);
}
}
fn ortho_path(pts: &[(f64, f64)]) -> String {
const R: f64 = 8.0;
if pts.len() < 3 {
return format!(
"M {} {} L {} {}",
fmt_f(pts[0].0),
fmt_f(pts[0].1),
fmt_f(pts[pts.len() - 1].0),
fmt_f(pts[pts.len() - 1].1)
);
}
let mut d = format!("M {} {}", fmt_f(pts[0].0), fmt_f(pts[0].1));
for i in 1..pts.len() - 1 {
let prev = pts[i - 1];
let cur = pts[i];
let next = pts[i + 1];
let in_len = ((cur.0 - prev.0).abs() + (cur.1 - prev.1).abs()).max(1e-6);
let out_len = ((next.0 - cur.0).abs() + (next.1 - cur.1).abs()).max(1e-6);
let r = R.min(in_len / 2.0).min(out_len / 2.0);
let in_dir = axis_dir(prev, cur);
let out_dir = axis_dir(cur, next);
let before = (cur.0 - in_dir.0 * r, cur.1 - in_dir.1 * r);
let after = (cur.0 + out_dir.0 * r, cur.1 + out_dir.1 * r);
d.push_str(&format!(
" L {} {} Q {} {}, {} {}",
fmt_f(before.0),
fmt_f(before.1),
fmt_f(cur.0),
fmt_f(cur.1),
fmt_f(after.0),
fmt_f(after.1),
));
}
let last = pts[pts.len() - 1];
d.push_str(&format!(" L {} {}", fmt_f(last.0), fmt_f(last.1)));
d
}
fn axis_dir(a: (f64, f64), b: (f64, f64)) -> (f64, f64) {
let dx = b.0 - a.0;
let dy = b.1 - a.1;
if dx.abs() >= dy.abs() {
(if dx >= 0.0 { 1.0 } else { -1.0 }, 0.0)
} else {
(0.0, if dy >= 0.0 { 1.0 } else { -1.0 })
}
}
fn end_tangent(pts: &[(f64, f64)], kind: RouteKind) -> ((f64, f64), (f64, f64)) {
let (a, b) = match kind {
RouteKind::Cubic => (pts[2], pts[3]),
_ => (pts[pts.len() - 2], pts[pts.len() - 1]),
};
(b, norm((b.0 - a.0, b.1 - a.1)))
}
fn start_tangent(pts: &[(f64, f64)], kind: RouteKind) -> ((f64, f64), (f64, f64)) {
let (a, b) = match kind {
RouteKind::Cubic => (pts[1], pts[0]),
_ => (pts[1], pts[0]),
};
(b, norm((b.0 - a.0, b.1 - a.1)))
}
fn set_endpoint(pts: &mut [(f64, f64)], _kind: RouteKind, end: bool, p: (f64, f64)) {
if end {
let n = pts.len();
pts[n - 1] = p;
} else {
pts[0] = p;
}
}
fn norm(v: (f64, f64)) -> (f64, f64) {
let len = (v.0 * v.0 + v.1 * v.1).sqrt();
if len < 1e-9 {
(1.0, 0.0)
} else {
(v.0 / len, v.1 / len)
}
}
fn draw_arrow(out: &mut String, apex: (f64, f64), dir: (f64, f64), len: f64, color: &str) {
let half_w = len * 0.42;
let base = (apex.0 - dir.0 * len, apex.1 - dir.1 * len);
let perp = (-dir.1, dir.0);
let p1 = (base.0 + perp.0 * half_w, base.1 + perp.1 * half_w);
let p2 = (base.0 - perp.0 * half_w, base.1 - perp.1 * half_w);
out.push_str(&format!(
r#"<path d="M {} {} L {} {} L {} {} Z" fill="{color}"/>"#,
fmt_f(apex.0),
fmt_f(apex.1),
fmt_f(p1.0),
fmt_f(p1.1),
fmt_f(p2.0),
fmt_f(p2.1),
));
out.push('\n');
}
fn draw_ports(out: &mut String, doc: &Document, g: &Geometry, p: &Palette) {
const PORT_LABEL_SIZE: f64 = 9.5;
for (&(eid, pi), &(x, y)) in &g.ports {
let el = doc.el(ElementId(eid));
let style = doc.resolved_style(el.id);
let stroke = p.resolve(style.color.as_ref(), p.node_border);
out.push_str(&format!(
r#"<circle cx="{}" cy="{}" r="3.2" fill="{}" stroke="{}" stroke-width="1.2"/>"#,
fmt_f(x),
fmt_f(y),
p.surface,
stroke
));
out.push('\n');
let port = &el.ports[pi];
if let Some(text) = &port.label {
use drawlang_core::model::Side;
let (lx, ly, anchor) = match port.side {
Side::Top => (x + 6.0, y - 7.0, ""),
Side::Bottom => (x + 6.0, y + 7.0 + PORT_LABEL_SIZE * 0.8, ""),
Side::Left => (x - 7.0, y - 6.0, r#" text-anchor="end""#),
Side::Right => (x + 7.0, y - 6.0, ""),
};
out.push_str(&format!(
r#"<text x="{}" y="{}" font-size="{}"{} fill="{}">{}</text>"#,
fmt_f(lx),
fmt_f(ly),
fmt_f(PORT_LABEL_SIZE),
anchor,
p.muted,
esc(text)
));
out.push('\n');
}
}
}