//! Tests for the artifact graph that convert it to Mermaid diagrams. use std::fmt::Write; use super::*; type NodeId = u32; type Edges = IndexMap<(NodeId, NodeId), EdgeInfo>; #[derive(Debug, Clone, PartialEq, Eq)] struct EdgeInfo { direction: EdgeDirection, flow: EdgeFlow, kind: EdgeKind, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum EdgeDirection { Forward, Backward, Bidirectional, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum EdgeFlow { SourceToTarget, TargetToSource, } impl EdgeFlow { #[must_use] fn reverse(&self) -> EdgeFlow { match self { EdgeFlow::SourceToTarget => EdgeFlow::TargetToSource, EdgeFlow::TargetToSource => EdgeFlow::SourceToTarget, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum EdgeKind { PathToSweep, Other, } impl EdgeDirection { #[must_use] fn merge(&self, other: EdgeDirection) -> EdgeDirection { match self { EdgeDirection::Forward => match other { EdgeDirection::Forward => EdgeDirection::Forward, EdgeDirection::Backward => EdgeDirection::Bidirectional, EdgeDirection::Bidirectional => EdgeDirection::Bidirectional, }, EdgeDirection::Backward => match other { EdgeDirection::Forward => EdgeDirection::Bidirectional, EdgeDirection::Backward => EdgeDirection::Backward, EdgeDirection::Bidirectional => EdgeDirection::Bidirectional, }, EdgeDirection::Bidirectional => EdgeDirection::Bidirectional, } } } impl Artifact { /// The IDs pointing back to prior nodes in a depth-first traversal of /// the graph. This should be disjoint with `child_ids`. pub(crate) fn back_edges(&self) -> Vec { match self { Artifact::CompositeSolid(a) => { let mut ids = a.solid_ids.clone(); ids.extend(a.tool_ids.iter()); ids } Artifact::Plane(_) => Vec::new(), Artifact::Path(a) => vec![a.plane_id], Artifact::Segment(a) => vec![a.path_id], Artifact::Solid2d(a) => vec![a.path_id], Artifact::StartSketchOnFace(a) => vec![a.face_id], Artifact::StartSketchOnPlane(a) => vec![a.plane_id], Artifact::Sweep(a) => vec![a.path_id], Artifact::Wall(a) => vec![a.seg_id, a.sweep_id], Artifact::Cap(a) => vec![a.sweep_id], Artifact::SweepEdge(a) => vec![a.seg_id, a.sweep_id], Artifact::EdgeCut(a) => vec![a.consumed_edge_id], Artifact::EdgeCutEdge(a) => vec![a.edge_cut_id], Artifact::Helix(a) => a.axis_id.map(|id| vec![id]).unwrap_or_default(), } } /// The child IDs of this artifact, used to do a depth-first traversal of /// the graph. pub(crate) fn child_ids(&self) -> Vec { match self { Artifact::CompositeSolid(a) => { // Note: Don't include these since they're parents: solid_ids, // tool_ids. let mut ids = Vec::new(); if let Some(composite_solid_id) = a.composite_solid_id { ids.push(composite_solid_id); } ids } Artifact::Plane(a) => a.path_ids.clone(), Artifact::Path(a) => { // Note: Don't include these since they're parents: plane_id. let mut ids = a.seg_ids.clone(); if let Some(sweep_id) = a.sweep_id { ids.push(sweep_id); } if let Some(solid2d_id) = a.solid2d_id { ids.push(solid2d_id); } if let Some(composite_solid_id) = a.composite_solid_id { ids.push(composite_solid_id); } ids } Artifact::Segment(a) => { // Note: Don't include these since they're parents: path_id. let mut ids = Vec::new(); if let Some(surface_id) = a.surface_id { ids.push(surface_id); } ids.extend(&a.edge_ids); if let Some(edge_cut_id) = a.edge_cut_id { ids.push(edge_cut_id); } ids.extend(&a.common_surface_ids); ids } Artifact::Solid2d(_) => { // Note: Don't include these since they're parents: path_id. Vec::new() } Artifact::StartSketchOnFace { .. } => { // Note: Don't include these since they're parents: face_id. Vec::new() } Artifact::StartSketchOnPlane { .. } => { // Note: Don't include these since they're parents: plane_id. Vec::new() } Artifact::Sweep(a) => { // Note: Don't include these since they're parents: path_id. let mut ids = Vec::new(); ids.extend(&a.surface_ids); ids.extend(&a.edge_ids); ids } Artifact::Wall(a) => { // Note: Don't include these since they're parents: seg_id, // sweep_id. let mut ids = Vec::new(); ids.extend(&a.edge_cut_edge_ids); ids.extend(&a.path_ids); ids } Artifact::Cap(a) => { // Note: Don't include these since they're parents: sweep_id. let mut ids = Vec::new(); ids.extend(&a.edge_cut_edge_ids); ids.extend(&a.path_ids); ids } Artifact::SweepEdge(a) => { // Note: Don't include these since they're parents: seg_id, // sweep_id. let mut ids = Vec::new(); ids.extend(&a.common_surface_ids); ids } Artifact::EdgeCut(a) => { // Note: Don't include these since they're parents: // consumed_edge_id. let mut ids = Vec::new(); ids.extend(&a.edge_ids); if let Some(surface_id) = a.surface_id { ids.push(surface_id); } ids } Artifact::EdgeCutEdge(a) => { // Note: Don't include these since they're parents: edge_cut_id. vec![a.surface_id] } Artifact::Helix(_) => { // Note: Don't include these since they're parents: axis_id. Vec::new() } } } } impl ArtifactGraph { /// Output the Mermaid flowchart for the artifact graph. pub(crate) fn to_mermaid_flowchart(&self) -> Result { let mut output = String::new(); output.push_str("```mermaid\n"); output.push_str("flowchart LR\n"); let mut next_id = 1_u32; let mut stable_id_map = FnvHashMap::default(); for id in self.map.keys() { stable_id_map.insert(*id, next_id); next_id = next_id.checked_add(1).unwrap(); } // Output all nodes first since edge order can change how Mermaid // lays out nodes. This is also where we output more details about // the nodes, like their labels. self.flowchart_nodes(&mut output, &stable_id_map, " ")?; self.flowchart_edges(&mut output, &stable_id_map, " ")?; output.push_str("```\n"); Ok(output) } /// Output the Mermaid flowchart nodes, one for each artifact. fn flowchart_nodes( &self, output: &mut W, stable_id_map: &FnvHashMap, prefix: &str, ) -> std::fmt::Result { // Artifact ID of the path is the key. The value is a list of // artifact IDs in that group. let mut groups = IndexMap::new(); let mut ungrouped = Vec::new(); for artifact in self.map.values() { let id = artifact.id(); let grouped = match artifact { Artifact::CompositeSolid(_) => false, Artifact::Plane(_) => false, Artifact::Path(_) => { groups.entry(id).or_insert_with(Vec::new).push(id); true } Artifact::Segment(segment) => { let path_id = segment.path_id; groups.entry(path_id).or_insert_with(Vec::new).push(id); true } Artifact::Solid2d(solid2d) => { let path_id = solid2d.path_id; groups.entry(path_id).or_insert_with(Vec::new).push(id); true } Artifact::StartSketchOnFace { .. } | Artifact::StartSketchOnPlane { .. } | Artifact::Sweep(_) | Artifact::Wall(_) | Artifact::Cap(_) | Artifact::SweepEdge(_) | Artifact::EdgeCut(_) | Artifact::EdgeCutEdge(_) | Artifact::Helix(_) => false, }; if !grouped { ungrouped.push(id); } } for (group_id, artifact_ids) in groups { let group_id = *stable_id_map.get(&group_id).unwrap(); writeln!(output, "{prefix}subgraph path{group_id} [Path]")?; let indented = format!("{} ", prefix); for artifact_id in artifact_ids { let artifact = self.map.get(&artifact_id).unwrap(); let id = *stable_id_map.get(&artifact_id).unwrap(); self.flowchart_node(output, artifact, id, &indented)?; } writeln!(output, "{prefix}end")?; } for artifact_id in ungrouped { let artifact = self.map.get(&artifact_id).unwrap(); let id = *stable_id_map.get(&artifact_id).unwrap(); self.flowchart_node(output, artifact, id, prefix)?; } Ok(()) } fn flowchart_node( &self, output: &mut W, artifact: &Artifact, id: NodeId, prefix: &str, ) -> std::fmt::Result { // For now, only showing the source range. fn code_ref_display(code_ref: &CodeRef) -> [usize; 3] { let range = code_ref.range; [range.start(), range.end(), range.module_id().as_usize()] } match artifact { Artifact::CompositeSolid(composite_solid) => { writeln!( output, "{prefix}{}[\"CompositeSolid {:?}
{:?}\"]", id, composite_solid.sub_type, code_ref_display(&composite_solid.code_ref) )?; } Artifact::Plane(plane) => { writeln!( output, "{prefix}{}[\"Plane
{:?}\"]", id, code_ref_display(&plane.code_ref) )?; } Artifact::Path(path) => { writeln!( output, "{prefix}{}[\"Path
{:?}\"]", id, code_ref_display(&path.code_ref) )?; } Artifact::Segment(segment) => { writeln!( output, "{prefix}{}[\"Segment
{:?}\"]", id, code_ref_display(&segment.code_ref) )?; } Artifact::Solid2d(_solid2d) => { writeln!(output, "{prefix}{}[Solid2d]", id)?; } Artifact::StartSketchOnFace(StartSketchOnFace { code_ref, .. }) => { writeln!( output, "{prefix}{}[\"StartSketchOnFace
{:?}\"]", id, code_ref_display(code_ref) )?; } Artifact::StartSketchOnPlane(StartSketchOnPlane { code_ref, .. }) => { writeln!( output, "{prefix}{}[\"StartSketchOnPlane
{:?}\"]", id, code_ref_display(code_ref) )?; } Artifact::Sweep(sweep) => { writeln!( output, "{prefix}{}[\"Sweep {:?}
{:?}\"]", id, sweep.sub_type, code_ref_display(&sweep.code_ref) )?; } Artifact::Wall(_wall) => { writeln!(output, "{prefix}{}[Wall]", id)?; } Artifact::Cap(cap) => { writeln!(output, "{prefix}{}[\"Cap {:?}\"]", id, cap.sub_type)?; } Artifact::SweepEdge(sweep_edge) => { writeln!(output, "{prefix}{}[\"SweepEdge {:?}\"]", id, sweep_edge.sub_type,)?; } Artifact::EdgeCut(edge_cut) => { writeln!( output, "{prefix}{}[\"EdgeCut {:?}
{:?}\"]", id, edge_cut.sub_type, code_ref_display(&edge_cut.code_ref) )?; } Artifact::EdgeCutEdge(_edge_cut_edge) => { writeln!(output, "{prefix}{}[EdgeCutEdge]", id)?; } Artifact::Helix(helix) => { writeln!( output, "{prefix}{}[\"Helix
{:?}\"]", id, code_ref_display(&helix.code_ref) )?; } } Ok(()) } fn flowchart_edges( &self, output: &mut W, stable_id_map: &FnvHashMap, prefix: &str, ) -> Result<(), std::fmt::Error> { // Mermaid will display two edges in either direction, even using // the `---` edge type. So we need to deduplicate them. fn add_unique_edge(edges: &mut Edges, source_id: NodeId, target_id: NodeId, flow: EdgeFlow, kind: EdgeKind) { if source_id == target_id { // Self edge. Skip it. return; } // The key is the node IDs in canonical order. let a = source_id.min(target_id); let b = source_id.max(target_id); let new_direction = if a == source_id { EdgeDirection::Forward } else { EdgeDirection::Backward }; let initial_flow = if a == source_id { flow } else { flow.reverse() }; let edge = edges.entry((a, b)).or_insert(EdgeInfo { direction: new_direction, flow: initial_flow, kind, }); // Merge with existing edge. edge.direction = edge.direction.merge(new_direction); } // Collect all edges to deduplicate them. let mut edges = IndexMap::default(); for artifact in self.map.values() { let source_id = *stable_id_map.get(&artifact.id()).unwrap(); // In Mermaid, the textual order defines the rank, even though the // edge arrow can go in either direction. // // Back edges: parent <- self // Child edges: self -> child for (target_id, flow) in artifact .back_edges() .into_iter() .zip(std::iter::repeat(EdgeFlow::TargetToSource)) .chain( artifact .child_ids() .into_iter() .zip(std::iter::repeat(EdgeFlow::SourceToTarget)), ) { let Some(target) = self.map.get(&target_id) else { continue; }; let edge_kind = match (artifact, target) { (Artifact::Path(_), Artifact::Sweep(_)) | (Artifact::Sweep(_), Artifact::Path(_)) => { EdgeKind::PathToSweep } _ => EdgeKind::Other, }; let target_id = *stable_id_map.get(&target_id).unwrap(); add_unique_edge(&mut edges, source_id, target_id, flow, edge_kind); } } // Output the edges. edges.par_sort_by(|ak, _, bk, _| (if ak.0 == bk.0 { ak.1.cmp(&bk.1) } else { ak.0.cmp(&bk.0) })); for ((source_id, target_id), edge) in edges { let extra = match edge.kind { // Extra length. This is needed to make the graph layout more // legible. Without it, the sweep will be at the same rank as // the path's segments, and the sweep's edges overlap with the // segment edges a lot. EdgeKind::PathToSweep => "-", EdgeKind::Other => "", }; match edge.flow { EdgeFlow::SourceToTarget => match edge.direction { EdgeDirection::Forward => { writeln!(output, "{prefix}{source_id} x{}--> {}", extra, target_id)?; } EdgeDirection::Backward => { writeln!(output, "{prefix}{source_id} <{}--x {}", extra, target_id)?; } EdgeDirection::Bidirectional => { writeln!(output, "{prefix}{source_id} {}--- {}", extra, target_id)?; } }, EdgeFlow::TargetToSource => match edge.direction { EdgeDirection::Forward => { writeln!(output, "{prefix}{target_id} x{}--> {}", extra, source_id)?; } EdgeDirection::Backward => { writeln!(output, "{prefix}{target_id} <{}--x {}", extra, source_id)?; } EdgeDirection::Bidirectional => { writeln!(output, "{prefix}{target_id} {}--- {}", extra, source_id)?; } }, } } Ok(()) } }