Svg output

This commit is contained in:
elvis
2025-11-02 15:03:07 +01:00
parent ea05a8d99e
commit 920b9c7280
5 changed files with 215 additions and 27 deletions

View File

@ -14,6 +14,8 @@ serde = { version = "1", optional = true }
colored = "*" colored = "*"
lalrpop-util = "*" lalrpop-util = "*"
petgraph = ">=0.8" petgraph = ">=0.8"
nsvg = "0.5.1"
dyn-clone = "*"
petgraph-graphml = "*" petgraph-graphml = "*"
egui_node_graph2 = { path = "../egui_node_graph2" } egui_node_graph2 = { path = "../egui_node_graph2" }
getrandom = "0.3" # dependency that has to be specified correctly for wasm getrandom = "0.3" # dependency that has to be specified correctly for wasm

View File

@ -63,6 +63,7 @@ pub enum BasicDataType {
PositiveGraph, PositiveGraph,
PositiveAssertFunction, PositiveAssertFunction,
PositiveGroupFunction, PositiveGroupFunction,
Svg,
} }
/// Should reflect `BasicDataType`'s values, holding the data that will be /// Should reflect `BasicDataType`'s values, holding the data that will be
@ -175,6 +176,9 @@ pub enum BasicValue {
PositiveGroupFunction { PositiveGroupFunction {
value: assert::positive_grouping::PositiveAssert, value: assert::positive_grouping::PositiveAssert,
}, },
Svg {
value: super::svg::Svg,
},
} }
impl Hash for BasicValue { impl Hash for BasicValue {
@ -216,7 +220,8 @@ impl Hash for BasicValue {
PositiveContext, PositiveContext,
PositiveReactions, PositiveReactions,
PositiveAssertFunction, PositiveAssertFunction,
PositiveGroupFunction PositiveGroupFunction,
Svg
); );
match self { match self {
@ -278,6 +283,7 @@ pub enum NodeInstruction {
DisplayEdge, DisplayEdge,
ColorNode, ColorNode,
ColorEdge, ColorEdge,
StringToSvg,
// convert basic data types // convert basic data types
ToPositiveSet, ToPositiveSet,
@ -486,6 +492,7 @@ impl NodeInstruction {
("display edge", DisplayEdge), ("display edge", DisplayEdge),
], ],
| Self::Sleep => vec![("seconds", PositiveInt)], | Self::Sleep => vec![("seconds", PositiveInt)],
| Self::StringToSvg => vec![("value", String)],
} }
.into_iter() .into_iter()
.map(|e| (e.0.to_string(), e.1)) .map(|e| (e.0.to_string(), e.1))
@ -581,6 +588,7 @@ impl NodeInstruction {
vec![("out", String)], vec![("out", String)],
| Self::PositiveBisimilarityPaigeTarjan => vec![("out", String)], | Self::PositiveBisimilarityPaigeTarjan => vec![("out", String)],
| Self::Sleep => vec![("out", PositiveInt)], | Self::Sleep => vec![("out", PositiveInt)],
| Self::StringToSvg => vec![("out", Svg)],
}; };
res.into_iter() res.into_iter()
.map(|res| (res.0.to_string(), res.1)) .map(|res| (res.0.to_string(), res.1))
@ -684,6 +692,10 @@ impl NodeInstruction {
PositiveGroupFunction, PositiveGroupFunction,
assert::positive_grouping::PositiveAssert::default() assert::positive_grouping::PositiveAssert::default()
), ),
| BasicDataType::Svg => helper!(
Svg,
super::svg::Svg::default()
)
} }
} }
@ -739,6 +751,8 @@ impl NodeInstruction {
helper!(PositiveAssertFunction), helper!(PositiveAssertFunction),
| BasicDataType::PositiveGroupFunction => | BasicDataType::PositiveGroupFunction =>
helper!(PositiveGroupFunction), helper!(PositiveGroupFunction),
| BasicDataType::Svg =>
helper!(Svg),
} }
} }
} }
@ -935,6 +949,8 @@ impl DataTypeTrait<GlobalState> for BasicDataType {
egui::Color32::from_rgb(200, 150, 120), egui::Color32::from_rgb(200, 150, 120),
| Self::PositiveGroupFunction => | Self::PositiveGroupFunction =>
egui::Color32::from_rgb(150, 120, 200), egui::Color32::from_rgb(150, 120, 200),
| Self::Svg =>
egui::Color32::from_rgb(200, 200, 240),
} }
} }
@ -972,6 +988,7 @@ impl DataTypeTrait<GlobalState> for BasicDataType {
Cow::Borrowed("positive assert function"), Cow::Borrowed("positive assert function"),
| Self::PositiveGroupFunction => | Self::PositiveGroupFunction =>
Cow::Borrowed("positive group function"), Cow::Borrowed("positive group function"),
| Self::Svg => Cow::Borrowed("Svg"),
} }
} }
} }
@ -1069,6 +1086,7 @@ impl NodeTemplateTrait for NodeInstruction {
| Self::PositiveBisimilarityPaigeTarjan => | Self::PositiveBisimilarityPaigeTarjan =>
"Positive Paige & Torjan", "Positive Paige & Torjan",
| Self::Sleep => "Sleep", | Self::Sleep => "Sleep",
| Self::StringToSvg => "String to SVG",
}) })
} }
@ -1150,7 +1168,8 @@ impl NodeTemplateTrait for NodeInstruction {
| Self::PositiveBisimilarityPaigeTarjanNoLabels | Self::PositiveBisimilarityPaigeTarjanNoLabels
| Self::PositiveBisimilarityPaigeTarjan => | Self::PositiveBisimilarityPaigeTarjan =>
vec!["Positive Graph", "Positive Bisimilarity"], vec!["Positive Graph", "Positive Bisimilarity"],
| Self::Sleep => vec!["General"], | Self::Sleep
| Self::StringToSvg => vec!["General"],
} }
} }
@ -1256,6 +1275,7 @@ impl NodeTemplateIter for AllInstructions {
NodeInstruction::PositiveBisimilarityPaigeTarjanNoLabels, NodeInstruction::PositiveBisimilarityPaigeTarjanNoLabels,
NodeInstruction::PositiveBisimilarityPaigeTarjan, NodeInstruction::PositiveBisimilarityPaigeTarjan,
NodeInstruction::Sleep, NodeInstruction::Sleep,
NodeInstruction::StringToSvg,
] ]
} }
} }
@ -1395,6 +1415,9 @@ impl WidgetValueTrait for BasicValue {
| BasicValue::PositiveGroupFunction { value: _ } => { | BasicValue::PositiveGroupFunction { value: _ } => {
ui.label(param_name); ui.label(param_name);
}, },
| BasicValue::Svg { value: _ } => {
ui.label(param_name);
}
} }
responses responses
@ -1523,7 +1546,7 @@ pub struct AppHandle {
translator: Arc<Mutex<rsprocess::translator::Translator>>, translator: Arc<Mutex<rsprocess::translator::Translator>>,
cached_last_value: Option<LayoutJob>, cached_last_value: Option<WidgetLayout>,
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
app_logic_thread: Option<JoinHandle<anyhow::Result<()>>>, app_logic_thread: Option<JoinHandle<anyhow::Result<()>>>,
@ -1870,15 +1893,15 @@ impl eframe::App for AppHandle {
user_state.display_result user_state.display_result
}; };
if display_result { if display_result {
let mut text = LayoutJob::default(); let mut content = WidgetLayout::default();
let mut spin = false; let mut spin = false;
if let Some(l_v) = &self.cached_last_value { if let Some(l_v) = &self.cached_last_value {
text = l_v.clone(); content = l_v.clone();
} else { } else {
#[cfg(not(target_arch = "wasm32"))] { #[cfg(not(target_arch = "wasm32"))] {
// wasm does not support threads :-( // wasm does not support threads :-(
// ------------------------------------------------------------- // ---------------------------------------------------------
// did we already start a thread? // did we already start a thread?
if self.app_logic_thread.is_none() { if self.app_logic_thread.is_none() {
let thread_join_handle = { let thread_join_handle = {
@ -1911,7 +1934,7 @@ impl eframe::App for AppHandle {
if let Err(e) = err { if let Err(e) = err {
let text = get_layout(Err(e), &self.translator.lock().unwrap(), ctx); let text = get_layout(Err(e), &self.translator.lock().unwrap(), ctx);
self.cached_last_value = Some(text.clone()); self.cached_last_value = Some(text);
} else if let Some(l_b_v) = self.cache.get_last_state() { } else if let Some(l_b_v) = self.cache.get_last_state() {
if let BasicValue::SaveString { path, value } = &l_b_v { if let BasicValue::SaveString { path, value } = &l_b_v {
use std::io::Write; use std::io::Write;
@ -1927,8 +1950,8 @@ impl eframe::App for AppHandle {
return; return;
} }
} }
text = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx); content = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx);
self.cached_last_value = Some(text.clone()); self.cached_last_value = Some(content.clone());
} }
} else { } else {
spin = true; spin = true;
@ -1945,7 +1968,7 @@ impl eframe::App for AppHandle {
); );
if let Err(e) = err { if let Err(e) = err {
let text = get_layout(Err(e), &self.translator.lock().unwrap(), ctx); let text = get_layout(Err(e), &self.translator.lock().unwrap(), ctx);
self.cached_last_value = Some(text.clone()); self.cached_last_value = Some(content.clone());
} else if let Some(l_b_v) = self.cache.get_last_state() { } else if let Some(l_b_v) = self.cache.get_last_state() {
if let BasicValue::SaveString { path, value } = &l_b_v { if let BasicValue::SaveString { path, value } = &l_b_v {
use std::io::Write; use std::io::Write;
@ -1961,8 +1984,8 @@ impl eframe::App for AppHandle {
return; return;
} }
} }
text = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx); content = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx);
self.cached_last_value = Some(text.clone()); self.cached_last_value = Some(content.clone());
} }
spin = false; spin = false;
} }
@ -1972,24 +1995,22 @@ impl eframe::App for AppHandle {
if spin { if spin {
window.show(ctx, |ui| { window.show(ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
use egui::widgets::Widget; use egui::widgets::Widget;
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.heading("Result"); ui.heading("Result");
}); });
ui.separator();
ui.vertical_centered(|ui| {
egui::widgets::Spinner::new().ui(ui); egui::widgets::Spinner::new().ui(ui);
}); });
}); });
} else { } else {
window.show(ctx, |ui| { window.show(ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.heading("Result"); ui.heading("Result");
}); });
egui::ScrollArea::vertical().show(ui, |ui| { ui.separator();
ui.label(text); ui.add(content);
});
});
}); });
} }
} }
@ -2044,11 +2065,40 @@ fn create_output(
Ok(()) Ok(())
} }
#[derive(Clone, Default)]
enum WidgetLayout {
LayoutJob(LayoutJob),
Image(egui::TextureHandle),
#[default]
Empty,
}
impl egui::Widget for WidgetLayout {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
match self {
| Self::LayoutJob(lj) => {
let mut response = None;
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
response = Some(egui::Label::new(lj).ui(ui));
});
response.unwrap()
},
| Self::Empty => {
egui::Label::new("").ui(ui)
},
| Self::Image(i) => {
egui::Image::new(&i).max_size(ui.available_size()).ui(ui)
}
}
}
}
fn get_layout( fn get_layout(
value: anyhow::Result<BasicValue>, value: anyhow::Result<BasicValue>,
translator: &rsprocess::translator::Translator, translator: &rsprocess::translator::Translator,
ctx: &egui::Context, ctx: &egui::Context,
) -> LayoutJob { ) -> WidgetLayout {
let mut text = LayoutJob::default(); let mut text = LayoutJob::default();
match value { match value {
@ -2231,6 +2281,9 @@ fn get_layout(
0., 0.,
Default::default(), Default::default(),
), ),
| BasicValue::Svg { value } => {
return WidgetLayout::Image(value.get_texture(ctx));
},
}, },
| Err(err) => { | Err(err) => {
text.append(&format!("{err:?}"), 0., TextFormat { text.append(&format!("{err:?}"), 0., TextFormat {
@ -2239,5 +2292,5 @@ fn get_layout(
}); });
}, },
} }
text WidgetLayout::LayoutJob(text)
} }

View File

@ -2492,7 +2492,27 @@ fn process_template(
#[cfg(target_arch = "wasm32")] { #[cfg(target_arch = "wasm32")] {
anyhow::bail!("Cannot sleep on wams"); anyhow::bail!("Cannot sleep on wams");
} }
},
| NodeInstruction::StringToSvg => {
let s = retrieve_from_cache![1];
let hash_inputs = hash_inputs!(s);
if let BasicValue::String { value } = s {
let res = match super::svg::Svg::parse_dot_string(&value) {
Ok(svg) => svg,
Err(e) => anyhow::bail!(e),
};
let res = BasicValue::Svg { value: res };
set_cache_output!((
output_names.first().unwrap(),
res,
hash_inputs
));
} else {
anyhow::bail!("Not a string");
} }
},
} }
Ok(None) Ok(None)
} }

View File

@ -6,6 +6,7 @@
mod app; mod app;
mod app_logic; mod app_logic;
mod helper; mod helper;
mod svg;
pub use app::AppHandle; pub use app::AppHandle;

View File

@ -0,0 +1,112 @@
use std::{fmt::Debug, hash::Hash, sync::{Arc, Mutex}};
use layout::{backends::svg::SVGWriter, gv::{self, GraphBuilder}};
use eframe::egui;
#[cfg_attr(
feature = "persistence",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Clone, Default)]
pub(crate) struct Svg {
image: egui::ColorImage,
/// original size of the svg
svg_size: egui::Vec2,
#[cfg_attr(feature = "persistence", serde(skip))]
svg_texture: Arc<Mutex<Option<egui::TextureHandle>>>,
}
impl Svg {
pub(crate) fn parse_dot_string(dot_str: &str) -> Result<Svg, String> {
let mut parser = gv::DotParser::new(dot_str);
let g = match parser.process() {
Ok(g) => g,
Err(_) =>
// errors are printed to sdtout so we ignore them
return Err("Could not parse dot string.".into()),
};
let mut gb = GraphBuilder::new();
gb.visit_graph(&g);
let mut graph = gb.get();
let mut svg = SVGWriter::new();
graph.do_it(
false,
false,
false,
&mut svg,
);
let content = svg.finalize();
let svg = match nsvg::parse_str(&content, nsvg::Units::Pixel, 96.0) {
Ok(svg) => svg,
Err(nsvg_err) => return Err(format!("{}", nsvg_err)),
};
let svg_size = egui::vec2(svg.width(), svg.height());
let (w, h, data) = match svg.rasterize_to_raw_rgba(1.) {
Ok(o) => o,
Err(e) => return Err(format!("{}", e)),
};
let image = egui::ColorImage::from_rgba_unmultiplied([w as _, h as _], &data);
let svg = Svg { image, svg_size, svg_texture: Arc::new(Mutex::new(None)) };
Ok(svg)
}
pub(crate) fn get_texture(&self, ctx: &egui::Context) -> egui::TextureHandle {
let tx = self.svg_texture.lock().expect("Poisoned");
if tx.is_some() {
(*tx).clone().unwrap()
} else {
std::mem::drop(tx);
let svg_texture = ctx.load_texture("svg", self.image.clone(), Default::default());
*self.svg_texture.lock().expect("Poisoned") = Some(svg_texture.clone());
svg_texture
}
}
}
impl Debug for Svg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[image: {:?}, svg_size: {:?}, svg_texture: {}",
self.image,
self.svg_size,
if self.svg_texture.lock().expect("Poisoned").is_some() {
"Some(...)"
} else {
"None"
}
)
}
}
impl Hash for Svg {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
macro_rules! hash_float {
($name:expr) => (
let bits = if $name.is_nan() {
// "Canonical" NaN.
0x7fc00000
} else {
// A trick taken from the `ordered-float` crate: -0.0 + 0.0 == +0.0.
// https://github.com/reem/rust-ordered-float/blob/1841f0541ea0e56779cbac03de2705149e020675/src/lib.rs#L2178-L2181
($name + 0.0).to_bits()
};
bits.hash(state);
);
}
hash_float!(self.svg_size.x);
hash_float!(self.svg_size.y);
self.image.pixels.hash(state);
self.image.size.hash(state);
hash_float!(self.image.source_size.x);
hash_float!(self.image.source_size.y);
self.svg_texture.lock().expect("Poisoned").hash(state);
}
}