Skip to main content

svg_export/
svg_export.rs

1//! Semantic consumption: a mini SVG exporter over the scene API.
2//!
3//! The point of this example is what it does NOT do: it never renders
4//! terminal text and never touches a charset. It plans once, then
5//! reads the scene's element views — resolved geometry, colors,
6//! weights, label slots — and projects them into SVG shapes. The same
7//! scene could drive a TUI, a canvas, or an editor overlay; the
8//! terminal emitters are just one more consumer of the same answers.
9//!
10//!   cargo run --example svg_export > graph.svg
11
12use ascii_dag::render::colors::Palette;
13use ascii_dag::{
14    EdgePathView, Graph, LabelSlot, LineWeight, NodeKind, RenderOptions, ScenePlanner,
15};
16use std::fmt::Write;
17
18/// One character cell in pixels.
19const CW: usize = 10;
20const CH: usize = 20;
21
22fn px(cell: (usize, usize)) -> (usize, usize) {
23    (cell.0 * CW + CW / 2, cell.1 * CH + CH / 2)
24}
25
26fn css(color: ascii_dag::CellColor) -> String {
27    match color.as_rgb() {
28        Some((r, g, b)) => format!("rgb({r},{g},{b})"),
29        None => "currentColor".into(),
30    }
31}
32
33fn main() {
34    let mut g = Graph::new();
35    g.add_node(1usize, "ingest");
36    g.add_node(2usize, "parse");
37    g.add_node(3usize, "check");
38    g.add_node(4usize, "emit");
39    g.add_edge(1usize, 2usize, Some("raw"));
40    g.add_edge(2usize, 3usize, None);
41    g.add_edge(2usize, 4usize, Some("fast path"));
42    g.add_edge(3usize, 4usize, None);
43    let sg = g.add_subgraph("backend");
44    g.put_nodes(&[2, 3]).inside(sg).unwrap();
45
46    let ir = g.compute_layout();
47
48    // Plan once (style callbacks run exactly here); read many.
49    let mut planner = ScenePlanner::new();
50    let scene = planner
51        .plan(&ir, &RenderOptions::colored(Palette::Ansi).plan)
52        .quiet()
53        .expect("plan");
54
55    let mut svg = String::new();
56    let (w, h) = (scene.width() * CW, scene.height() * CH + 24);
57    let _ = writeln!(
58        svg,
59        r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" font-family="monospace" font-size="13">"#
60    );
61
62    // Clusters first (background), from resolved subgraph views.
63    for s in scene.subgraphs() {
64        let _ = writeln!(
65            svg,
66            r#"  <rect x="{}" y="{}" width="{}" height="{}" fill="none" stroke="{}" stroke-dasharray="4 3" rx="6"/>"#,
67            s.x * CW,
68            s.y * CH,
69            s.width * CW,
70            s.height * CH,
71            css(s.color),
72        );
73        let _ = writeln!(
74            svg,
75            r#"  <text x="{}" y="{}" opacity="0.7">{}</text>"#,
76            s.x * CW + 8,
77            s.y * CH + 15,
78            s.label,
79        );
80    }
81
82    // Edges: routed geometry as polylines, resolved style as stroke.
83    // (This graph flows top-down, so every trunk is Y-axis; a general
84    // exporter would branch on `e.flow_axis` for the horizontal
85    // reading of `Corner`/`SideChannel`.)
86    for e in scene.edges() {
87        let mut pts = vec![px(e.from)];
88        match e.path {
89            EdgePathView::Direct => {}
90            EdgePathView::Corner { bend_at } => {
91                pts.push(px((e.from.0, bend_at)));
92                pts.push(px((e.to.0, bend_at)));
93            }
94            EdgePathView::SideChannel {
95                channel_at,
96                span_start,
97                span_end,
98            } => {
99                pts.push(px((e.from.0, span_start)));
100                pts.push(px((channel_at, span_start)));
101                pts.push(px((channel_at, span_end)));
102                pts.push(px((e.to.0, span_end)));
103            }
104            EdgePathView::MultiSegment { waypoints, .. } => {
105                pts.extend(waypoints.iter().map(|&p| px(p)));
106            }
107            _ => {}
108        }
109        pts.push(px(e.to));
110        let points: Vec<String> = pts.iter().map(|(x, y)| format!("{x},{y}")).collect();
111        let dash = match e.weight {
112            LineWeight::Dashed => r#" stroke-dasharray="5 4""#,
113            _ => "",
114        };
115        let _ = writeln!(
116            svg,
117            r#"  <polyline points="{}" fill="none" stroke="{}"{dash} marker-end="url(#arrow)"/>"#,
118            points.join(" "),
119            css(e.color),
120        );
121        // Labels go where the PLAN put them: inline at their resolved
122        // cell, or collected below if they overflowed to the legend.
123        if let Some(label) = e.label {
124            if let LabelSlot::Inline { x, y, .. } = label.slot {
125                let _ = writeln!(
126                    svg,
127                    r#"  <text x="{}" y="{}" fill="{}">{}</text>"#,
128                    x * CW,
129                    y * CH + 14,
130                    css(label.color),
131                    label.text,
132                );
133            }
134        }
135    }
136
137    // Nodes above edges (the same z-order hit-testing uses).
138    for n in scene.nodes() {
139        if matches!(n.kind, NodeKind::Dummy) {
140            continue;
141        }
142        let _ = writeln!(
143            svg,
144            r#"  <rect x="{}" y="{}" width="{}" height="{}" fill="white" stroke="black" rx="3"/>"#,
145            n.x * CW,
146            n.y * CH,
147            n.width * CW,
148            n.height.max(1) * CH,
149        );
150        let _ = writeln!(
151            svg,
152            r#"  <text x="{}" y="{}" text-anchor="middle">{}</text>"#,
153            (n.x + n.width / 2) * CW,
154            n.y * CH + 15,
155            n.label,
156        );
157    }
158
159    // Overflowed labels: the legend list, below the canvas.
160    for (i, e) in scene.legend().enumerate() {
161        if let Some(label) = e.label {
162            let _ = writeln!(
163                svg,
164                r#"  <text x="4" y="{}" fill="{}">{} → {}: {}</text>"#,
165                scene.height() * CH + 16 + i * 16,
166                css(label.color),
167                e.from_id,
168                e.to_id,
169                label.text,
170            );
171        }
172    }
173
174    let _ = writeln!(
175        svg,
176        r#"  <defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker></defs>"#
177    );
178    let _ = writeln!(svg, "</svg>");
179    println!("{svg}");
180}