render_regex 0.0.0

SVG visualization of regex DFAs.
Documentation
// src/plan.rs — Converts a VisualGraph + RenderProfile into a fully-resolved RenderIntentGraph

use crate::graph::VisualGraph;
use crate::intent::{RenderIntentGraph, RenderIntentNode, RenderIntentEdge, CurveType};
use crate::layout::strategies::compute_layout;
use crate::layout::profile::{RenderProfile, FanoutMode};
use rand::Rng;
use std::collections::HashMap;


pub struct IntentBuilder;

impl IntentBuilder {
    pub fn from_graph(graph: &VisualGraph, profile: &RenderProfile) -> RenderIntentGraph {
        let layout = compute_layout(graph, &profile.layout, profile.spacing);

        // Build nodes
        let mut nodes = Vec::new();
        let mut node_index = HashMap::new();
        for node in &graph.nodes {
            let (x, y) = layout.get(&node.id).copied().unwrap_or((0.0, 0.0));
            let intent_node = RenderIntentNode {
                id: node.id,
                label: node.label.clone(),
                center: (x, y),
                radius: 18.0,
                shape: node.shape,
                fill_color: "white".into(),
                stroke_color: "black".into(),
                stroke_width: 2.0,
                font_size: profile.font_size as f64,
                font_family: "sans-serif".into(),
                font_color: "black".into(),
                label_offset: (0.0, 5.0),
                is_accept: node.is_accept,
		is_start:  node.id == 0,
                node_type: node.node_type,
            };
	    println!("Node {} :: start: {}; end: {}", intent_node.id, intent_node.is_start, intent_node.is_accept);
            node_index.insert(node.id, intent_node.clone());
            nodes.push(intent_node);
        }

        // Group edges by unordered node-pair
        let mut edge_groups: HashMap<(usize, usize), Vec<&_>> = HashMap::new();
        for edge in &graph.edges {
            let key = if edge.from < edge.to {
                (edge.from, edge.to)
            } else {
                (edge.to, edge.from)
            };
            edge_groups.entry(key).or_default().push(edge);
        }

        // Build edges with classification: Loop | Cubic | Straight
        let mut edges = Vec::new();
        for group in edge_groups.values() {
            let n = group.len();
            let mid = (n - 1) as f64 / 2.0;

            for (i, edge) in group.iter().enumerate() {
                let from_node = &node_index[&edge.from];
                let to_node   = &node_index[&edge.to];

                let (x1, y1) = from_node.center;
                let (x2, y2) = to_node.center;

                // Classify curve
                let curve = if edge.from == edge.to {
                    // Self-loop
                    CurveType::Loop {
                        angle: profile.loop_angle as f64,
                        offset: 1.0,
                    }
                } else if profile.no_fanout {
                    // All edges straight
                    CurveType::Straight
                } else if n > 1 {
                    // Fan-out: multiple edges => Cubic
                    // Compute factor based on mode
                    let idx = i as f64 - mid;
                    let factor = match profile.fanout_mode {
                        FanoutMode::Symmetric => idx.signum(),
                        FanoutMode::Offset    => i as f64,
                        FanoutMode::Random    => {
                            let mut rng = rand::thread_rng();
                            rng.gen_range(-1.0..1.0) * idx.abs()
                        }
                    };
                    let bend = profile.fanout_max_bend as f64 * factor;

                    // Build cubic control points
                    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 base = ((x1 + x2) / 2.0, (y1 + y2) / 2.0);
                    let control1 = (base.0 + px * bend * 0.6, base.1 + py * bend * 0.6);
                    let control2 = (base.0 + px * bend * 1.2, base.1 + py * bend * 1.2);

                    CurveType::Cubic { control1, control2 }
                } else {
                    // Single edge => Straight
                    CurveType::Straight
                };

                edges.push(RenderIntentEdge {
                    from: edge.from,
                    to: edge.to,
                    from_point: (x1, y1),
                    to_point:   (x2, y2),
                    curve,
                    bend_offset: 0.0,
                    stroke_color: "black".into(),
                    stroke_width: 2.0,
                    arrow_size:   8.0,
                    arrow_refx:   6.0,
                    arrow_fill:   "black".into(),
                    label: edge.label.clone(),
                    label_pos: ((x1 + x2) / 2.0, (y1 + y2) / 2.0 - 5.0),
                    label_font_size:  profile.font_size as f64,
                    label_font_color: "black".into(),
                });
            }
        }

        RenderIntentGraph {
            nodes,
            edges,
            width: 600.0,
            height: 400.0,
        }
    }
}