Svg output
This commit is contained in:
@ -14,6 +14,8 @@ serde = { version = "1", optional = true }
|
||||
colored = "*"
|
||||
lalrpop-util = "*"
|
||||
petgraph = ">=0.8"
|
||||
nsvg = "0.5.1"
|
||||
dyn-clone = "*"
|
||||
petgraph-graphml = "*"
|
||||
egui_node_graph2 = { path = "../egui_node_graph2" }
|
||||
getrandom = "0.3" # dependency that has to be specified correctly for wasm
|
||||
|
||||
@ -63,6 +63,7 @@ pub enum BasicDataType {
|
||||
PositiveGraph,
|
||||
PositiveAssertFunction,
|
||||
PositiveGroupFunction,
|
||||
Svg,
|
||||
}
|
||||
|
||||
/// Should reflect `BasicDataType`'s values, holding the data that will be
|
||||
@ -175,6 +176,9 @@ pub enum BasicValue {
|
||||
PositiveGroupFunction {
|
||||
value: assert::positive_grouping::PositiveAssert,
|
||||
},
|
||||
Svg {
|
||||
value: super::svg::Svg,
|
||||
},
|
||||
}
|
||||
|
||||
impl Hash for BasicValue {
|
||||
@ -216,7 +220,8 @@ impl Hash for BasicValue {
|
||||
PositiveContext,
|
||||
PositiveReactions,
|
||||
PositiveAssertFunction,
|
||||
PositiveGroupFunction
|
||||
PositiveGroupFunction,
|
||||
Svg
|
||||
);
|
||||
|
||||
match self {
|
||||
@ -278,6 +283,7 @@ pub enum NodeInstruction {
|
||||
DisplayEdge,
|
||||
ColorNode,
|
||||
ColorEdge,
|
||||
StringToSvg,
|
||||
|
||||
// convert basic data types
|
||||
ToPositiveSet,
|
||||
@ -486,6 +492,7 @@ impl NodeInstruction {
|
||||
("display edge", DisplayEdge),
|
||||
],
|
||||
| Self::Sleep => vec![("seconds", PositiveInt)],
|
||||
| Self::StringToSvg => vec![("value", String)],
|
||||
}
|
||||
.into_iter()
|
||||
.map(|e| (e.0.to_string(), e.1))
|
||||
@ -581,6 +588,7 @@ impl NodeInstruction {
|
||||
vec![("out", String)],
|
||||
| Self::PositiveBisimilarityPaigeTarjan => vec![("out", String)],
|
||||
| Self::Sleep => vec![("out", PositiveInt)],
|
||||
| Self::StringToSvg => vec![("out", Svg)],
|
||||
};
|
||||
res.into_iter()
|
||||
.map(|res| (res.0.to_string(), res.1))
|
||||
@ -684,6 +692,10 @@ impl NodeInstruction {
|
||||
PositiveGroupFunction,
|
||||
assert::positive_grouping::PositiveAssert::default()
|
||||
),
|
||||
| BasicDataType::Svg => helper!(
|
||||
Svg,
|
||||
super::svg::Svg::default()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -739,6 +751,8 @@ impl NodeInstruction {
|
||||
helper!(PositiveAssertFunction),
|
||||
| BasicDataType::PositiveGroupFunction =>
|
||||
helper!(PositiveGroupFunction),
|
||||
| BasicDataType::Svg =>
|
||||
helper!(Svg),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -935,6 +949,8 @@ impl DataTypeTrait<GlobalState> for BasicDataType {
|
||||
egui::Color32::from_rgb(200, 150, 120),
|
||||
| Self::PositiveGroupFunction =>
|
||||
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"),
|
||||
| Self::PositiveGroupFunction =>
|
||||
Cow::Borrowed("positive group function"),
|
||||
| Self::Svg => Cow::Borrowed("Svg"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1069,6 +1086,7 @@ impl NodeTemplateTrait for NodeInstruction {
|
||||
| Self::PositiveBisimilarityPaigeTarjan =>
|
||||
"Positive Paige & Torjan",
|
||||
| Self::Sleep => "Sleep",
|
||||
| Self::StringToSvg => "String to SVG",
|
||||
})
|
||||
}
|
||||
|
||||
@ -1150,7 +1168,8 @@ impl NodeTemplateTrait for NodeInstruction {
|
||||
| Self::PositiveBisimilarityPaigeTarjanNoLabels
|
||||
| Self::PositiveBisimilarityPaigeTarjan =>
|
||||
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::PositiveBisimilarityPaigeTarjan,
|
||||
NodeInstruction::Sleep,
|
||||
NodeInstruction::StringToSvg,
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1395,6 +1415,9 @@ impl WidgetValueTrait for BasicValue {
|
||||
| BasicValue::PositiveGroupFunction { value: _ } => {
|
||||
ui.label(param_name);
|
||||
},
|
||||
| BasicValue::Svg { value: _ } => {
|
||||
ui.label(param_name);
|
||||
}
|
||||
}
|
||||
|
||||
responses
|
||||
@ -1523,7 +1546,7 @@ pub struct AppHandle {
|
||||
|
||||
translator: Arc<Mutex<rsprocess::translator::Translator>>,
|
||||
|
||||
cached_last_value: Option<LayoutJob>,
|
||||
cached_last_value: Option<WidgetLayout>,
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
app_logic_thread: Option<JoinHandle<anyhow::Result<()>>>,
|
||||
@ -1870,15 +1893,15 @@ impl eframe::App for AppHandle {
|
||||
user_state.display_result
|
||||
};
|
||||
if display_result {
|
||||
let mut text = LayoutJob::default();
|
||||
let mut content = WidgetLayout::default();
|
||||
let mut spin = false;
|
||||
|
||||
if let Some(l_v) = &self.cached_last_value {
|
||||
text = l_v.clone();
|
||||
content = l_v.clone();
|
||||
} else {
|
||||
#[cfg(not(target_arch = "wasm32"))] {
|
||||
// wasm does not support threads :-(
|
||||
// -------------------------------------------------------------
|
||||
// ---------------------------------------------------------
|
||||
// did we already start a thread?
|
||||
if self.app_logic_thread.is_none() {
|
||||
let thread_join_handle = {
|
||||
@ -1911,7 +1934,7 @@ impl eframe::App for AppHandle {
|
||||
|
||||
if let Err(e) = err {
|
||||
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() {
|
||||
if let BasicValue::SaveString { path, value } = &l_b_v {
|
||||
use std::io::Write;
|
||||
@ -1927,8 +1950,8 @@ impl eframe::App for AppHandle {
|
||||
return;
|
||||
}
|
||||
}
|
||||
text = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx);
|
||||
self.cached_last_value = Some(text.clone());
|
||||
content = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx);
|
||||
self.cached_last_value = Some(content.clone());
|
||||
}
|
||||
} else {
|
||||
spin = true;
|
||||
@ -1945,7 +1968,7 @@ impl eframe::App for AppHandle {
|
||||
);
|
||||
if let Err(e) = err {
|
||||
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() {
|
||||
if let BasicValue::SaveString { path, value } = &l_b_v {
|
||||
use std::io::Write;
|
||||
@ -1961,8 +1984,8 @@ impl eframe::App for AppHandle {
|
||||
return;
|
||||
}
|
||||
}
|
||||
text = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx);
|
||||
self.cached_last_value = Some(text.clone());
|
||||
content = get_layout(Ok(l_b_v), &self.translator.lock().unwrap(), ctx);
|
||||
self.cached_last_value = Some(content.clone());
|
||||
}
|
||||
spin = false;
|
||||
}
|
||||
@ -1972,24 +1995,22 @@ impl eframe::App for AppHandle {
|
||||
|
||||
if spin {
|
||||
window.show(ctx, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
use egui::widgets::Widget;
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Result");
|
||||
});
|
||||
use egui::widgets::Widget;
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Result");
|
||||
});
|
||||
ui.separator();
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::widgets::Spinner::new().ui(ui);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
window.show(ctx, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Result");
|
||||
});
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.label(text);
|
||||
});
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Result");
|
||||
});
|
||||
ui.separator();
|
||||
ui.add(content);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2044,11 +2065,40 @@ fn create_output(
|
||||
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(
|
||||
value: anyhow::Result<BasicValue>,
|
||||
translator: &rsprocess::translator::Translator,
|
||||
ctx: &egui::Context,
|
||||
) -> LayoutJob {
|
||||
) -> WidgetLayout {
|
||||
let mut text = LayoutJob::default();
|
||||
|
||||
match value {
|
||||
@ -2231,6 +2281,9 @@ fn get_layout(
|
||||
0.,
|
||||
Default::default(),
|
||||
),
|
||||
| BasicValue::Svg { value } => {
|
||||
return WidgetLayout::Image(value.get_texture(ctx));
|
||||
},
|
||||
},
|
||||
| Err(err) => {
|
||||
text.append(&format!("{err:?}"), 0., TextFormat {
|
||||
@ -2239,5 +2292,5 @@ fn get_layout(
|
||||
});
|
||||
},
|
||||
}
|
||||
text
|
||||
WidgetLayout::LayoutJob(text)
|
||||
}
|
||||
|
||||
@ -2492,7 +2492,27 @@ fn process_template(
|
||||
#[cfg(target_arch = "wasm32")] {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
mod app;
|
||||
mod app_logic;
|
||||
mod helper;
|
||||
mod svg;
|
||||
|
||||
pub use app::AppHandle;
|
||||
|
||||
|
||||
112
reaction_systems_gui/src/svg.rs
Normal file
112
reaction_systems_gui/src/svg.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user