use crate::engines::graph::algorithms::layered::MeasurementMode;
use crate::engines::graph::contracts::GraphGeometryContract;
use crate::engines::graph::{
EngineAlgorithmId, EngineConfig, EngineId, GraphSolveRequest, GraphSolveResult,
SubgraphDirectionPolicy, solve_graph_family,
};
use crate::errors::RenderError;
use crate::format::OutputFormat;
use crate::graph::measure::{
DEFAULT_PROPORTIONAL_FONT_SIZE, DEFAULT_PROPORTIONAL_NODE_PADDING_X,
DEFAULT_PROPORTIONAL_NODE_PADDING_Y, ProportionalTextMetrics,
};
use crate::graph::{GeometryLevel, Graph};
use crate::render::graph::{
SvgRenderOptions, render_svg_from_geometry_with_theme_and_routing,
render_svg_from_routed_geometry_with_theme, render_text_from_geometry,
};
use crate::runtime::config::RenderConfig;
use crate::runtime::resolve_configured_svg_theme;
use crate::simplification::PathSimplification;
pub(in crate::runtime) fn render_graph_family(
diagram_id: &str,
diagram: &Graph,
format: OutputFormat,
config: &RenderConfig,
) -> Result<String, RenderError> {
let engine_id = config
.layout_engine
.unwrap_or(EngineAlgorithmId::FLUX_LAYERED);
engine_id.check_available()?;
engine_id.check_routing_style(
config
.routing_style
.or_else(|| config.edge_preset.map(|preset| preset.expand().0)),
)?;
let request = graph_solve_request_for(format, config, diagram_id);
let engine_config = EngineConfig::Layered(config.layout.clone().into());
let engine_id = resolve_graph_engine_for_request(engine_id, &request);
let result = solve_graph_family(diagram, engine_id, &engine_config, &request)?;
match format {
OutputFormat::Mmds => render_mmds_from_solve_result(
diagram_id,
diagram,
&result,
config.geometry_level,
config.path_simplification,
),
OutputFormat::Svg => {
let options = config.svg_render_options();
Ok(render_svg_from_solve_result(
diagram, &result, &options, config,
)?)
}
OutputFormat::Text | OutputFormat::Ascii => {
let options = config.text_render_options(format);
Ok(render_text_from_geometry(
diagram,
&result.geometry,
result.routed.as_ref(),
&options,
))
}
_ => Err(RenderError {
message: format!("{format} output is not supported for {diagram_id} diagrams"),
}),
}
}
fn subgraph_direction_policy_for(diagram_id: &str) -> SubgraphDirectionPolicy {
match diagram_id {
"flowchart" => SubgraphDirectionPolicy::AlternateAxes,
_ => SubgraphDirectionPolicy::Preserve,
}
}
fn graph_solve_request_for(
format: OutputFormat,
config: &RenderConfig,
diagram_id: &str,
) -> GraphSolveRequest {
let routing_style = config
.routing_style
.or_else(|| config.edge_preset.map(|preset| preset.expand().0));
GraphSolveRequest::new(
measurement_mode_for_format(format, config),
geometry_contract_for_format(format),
config.geometry_level,
routing_style,
subgraph_direction_policy_for(diagram_id),
)
}
fn measurement_mode_for_format(format: OutputFormat, config: &RenderConfig) -> MeasurementMode {
match format {
OutputFormat::Svg | OutputFormat::Mmds => {
MeasurementMode::Proportional(proportional_text_metrics_for_config(config))
}
_ => MeasurementMode::Grid,
}
}
fn geometry_contract_for_format(format: OutputFormat) -> GraphGeometryContract {
match format {
OutputFormat::Svg => GraphGeometryContract::Visual,
_ => GraphGeometryContract::Canonical,
}
}
fn proportional_text_metrics_for_config(config: &RenderConfig) -> ProportionalTextMetrics {
let node_padding_x = config
.svg_node_padding_x
.unwrap_or(DEFAULT_PROPORTIONAL_NODE_PADDING_X);
let node_padding_y = config
.svg_node_padding_y
.unwrap_or(DEFAULT_PROPORTIONAL_NODE_PADDING_Y);
ProportionalTextMetrics::new(
DEFAULT_PROPORTIONAL_FONT_SIZE,
node_padding_x,
node_padding_y,
)
}
fn resolve_graph_engine_for_request(
engine_id: EngineAlgorithmId,
request: &GraphSolveRequest,
) -> EngineAlgorithmId {
if engine_id.engine() == EngineId::Mermaid
&& matches!(request.measurement_mode, MeasurementMode::Grid)
{
EngineAlgorithmId::new(EngineId::Flux, engine_id.algorithm())
} else {
engine_id
}
}
fn render_svg_from_solve_result(
diagram: &Graph,
result: &GraphSolveResult,
options: &SvgRenderOptions,
config: &RenderConfig,
) -> Result<String, RenderError> {
let theme = resolve_configured_svg_theme(config)?;
Ok(match result.routed.as_ref() {
Some(routed) => {
render_svg_from_routed_geometry_with_theme(diagram, routed, options, theme.as_ref())
}
None => render_svg_from_geometry_with_theme_and_routing(
diagram,
&result.geometry,
options,
crate::render::graph::edge_routing_from_style(options.routing_style),
theme.as_ref(),
),
})
}
fn render_mmds_from_solve_result(
diagram_type: &str,
diagram: &Graph,
result: &GraphSolveResult,
level: GeometryLevel,
path_simplification: PathSimplification,
) -> Result<String, RenderError> {
let engine_id = result.engine_id.to_string();
crate::mmds::to_json_typed_with_routing(
diagram_type,
diagram,
&result.geometry,
result.routed.as_ref(),
level,
path_simplification,
Some(engine_id.as_str()),
)
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::Path;
use super::*;
use crate::builtins::default_registry;
use crate::graph::{Direction, Edge, Graph, Node};
use crate::payload::Diagram as Payload;
use crate::render::graph::TextRenderOptions;
use crate::runtime::config::RenderConfig;
fn graph_fixture(input: &str) -> Graph {
let payload = default_registry()
.create("flowchart")
.expect("flowchart should be registered")
.parse(input)
.expect("fixture should parse")
.into_payload()
.expect("fixture should build a payload");
let Payload::Flowchart(graph) = payload else {
panic!("flowchart should yield a flowchart payload");
};
graph
}
fn graph_solve_result_fixture() -> (Graph, GraphSolveResult) {
let diagram = graph_fixture("graph TD\n A[Start] --> B[End]\n");
let request = GraphSolveRequest::new(
MeasurementMode::Grid,
GraphGeometryContract::Canonical,
GeometryLevel::Layout,
None,
Default::default(),
);
let result = solve_graph_family(
&diagram,
EngineAlgorithmId::FLUX_LAYERED,
&EngineConfig::Layered(Default::default()),
&request,
)
.expect("graph solve should succeed");
(diagram, result)
}
#[test]
fn text_renderer_consumes_graph_solve_result() {
let (diagram, result) = graph_solve_result_fixture();
let text = render_text_from_geometry(
&diagram,
&result.geometry,
result.routed.as_ref(),
&TextRenderOptions::default(),
);
assert!(text.contains("Start"));
}
#[test]
fn svg_renderer_consumes_graph_solve_result() {
let (diagram, result) = graph_solve_result_fixture();
let svg = render_svg_from_solve_result(
&diagram,
&result,
&SvgRenderOptions::default(),
&RenderConfig::default(),
)
.expect("SVG render should succeed");
assert!(svg.starts_with("<svg"));
}
#[test]
fn mmds_renderer_consumes_graph_solve_result() {
let (diagram, result) = graph_solve_result_fixture();
let json = render_mmds_from_solve_result(
"flowchart",
&diagram,
&result,
GeometryLevel::Routed,
PathSimplification::default(),
)
.expect("MMDS render should succeed");
assert!(json.contains("\"nodes\""));
}
#[test]
fn runtime_owner_local_smoke_renders_graph_family_text() {
let rendered = super::render_graph_family(
"flowchart",
&smoke_diagram(),
OutputFormat::Text,
&RenderConfig::default(),
)
.expect("runtime graph-family smoke render should succeed");
assert!(rendered.contains("Start"));
}
#[test]
fn runtime_entrypoint_dispatches_mmds_input_through_frontend() {
let input = mmds_fixture("minimal-layout.json");
let diagram_id =
crate::detect_diagram(&input).expect("runtime should resolve MMDS fixture");
assert_eq!(diagram_id, "flowchart");
let output = crate::render_diagram(&input, OutputFormat::Text, &RenderConfig::default())
.expect("layout MMDS payload should render via runtime frontend dispatch");
assert!(output.contains("Start"));
assert!(output.contains("End"));
}
fn mmds_fixture(name: &str) -> String {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("mmds")
.join(name);
fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()))
}
fn smoke_diagram() -> Graph {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Start"));
diagram.add_node(Node::new("B").with_label("End"));
diagram.add_edge(Edge::new("A", "B"));
diagram
}
}