diff options
Diffstat (limited to 'packages/svgbob/src/util.rs')
-rw-r--r-- | packages/svgbob/src/util.rs | 501 |
1 files changed, 501 insertions, 0 deletions
diff --git a/packages/svgbob/src/util.rs b/packages/svgbob/src/util.rs new file mode 100644 index 0000000..db63681 --- /dev/null +++ b/packages/svgbob/src/util.rs @@ -0,0 +1,501 @@ +use crate::Point; +use parry2d::shape::Triangle; +use parry2d::{bounding_volume::AABB, math::Isometry, query::PointQuery}; +use std::cmp::Ordering; + +pub fn opt_ord(f1: Option<f32>, f2: Option<f32>) -> Ordering { + match (f1, f2) { + (None, None) => Ordering::Equal, + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (Some(f1), Some(f2)) => ord(f1, f2), + } +} + +#[track_caller] +pub fn ord(f1: f32, f2: f32) -> Ordering { + if f1 == f2 { + Ordering::Equal + } else if f1 > f2 { + Ordering::Greater + } else if f1 < f2 { + Ordering::Less + } else { + println!("f1: {}, f2: {}", f1, f2); + log::error!("f1: {}, f2: {}", f1, f2); + unreachable!("comparison should only be 3 possibilities") + } +} + +/// clips a line to the bounding box of this whole grid +/// and approximate each point to the closes intersection +fn clip_line_internal( + aabb: &AABB, + start: Point, + end: Point, +) -> Option<(Point, Point)> { + let start_v = start.to_vector(); + let end_v = end.to_vector() - start_v; + let clipped = aabb.clip_line(&start, &end_v); + if let Some(clipped) = clipped { + let a = *clipped.a; + let b = *clipped.b; + Some((a.into(), b.into())) + } else { + None + } +} + +/// clip a line but do not extend the points +pub fn clip_line( + aabb: &AABB, + start: Point, + end: Point, +) -> Option<(Point, Point)> { + let clipped = clip_line_internal(aabb, start, end); + let identity = &Isometry::identity(); + if let Some(clipped) = clipped { + let mut clip_start = clipped.0; + let mut clip_end = clipped.1; + + if aabb.contains_point(identity, &start) { + clip_start = start; + } + if aabb.contains_point(identity, &end) { + clip_end = end; + } + if clip_start == clip_end { + None + } else { + Some((clip_start, clip_end)) + } + } else { + None + } +} + +/// the threshold are of 0.01 is used since +/// lines may not be very aligned. +pub fn is_collinear(a: &Point, b: &Point, c: &Point) -> bool { + use std::ops::Deref; + Triangle::new(*a.deref(), *b.deref(), *c.deref()).area() < 0.01 +} + +pub fn pad(v: f32) -> f32 { + if v > 0.0 { + v.ceil() + } else { + v.floor() + } +} + +/// this is parser module which provides parsing for identifier for +/// extracting the css tag of inside of a shape fragment +pub mod parser { + + use pom::parser::{is_a, list, none_of, one_of, sym, tag, Parser}; + use std::iter::FromIterator; + + /// Parses a list with the defined separator, but will fail early when one of the + /// item can not be parsed + pub fn list_fail<'a, I, O, U>( + parser: Parser<'a, I, O>, + separator: Parser<'a, I, U>, + ) -> Parser<'a, I, Vec<O>> + where + O: 'a, + U: 'a, + { + Parser::new(move |input: &'a [I], start: usize| { + let mut items = vec![]; + let mut pos = start; + match (parser.method)(input, pos) { + Ok((first_item, first_pos)) => { + items.push(first_item); + pos = first_pos; + loop { + match (separator.method)(input, pos) { + Ok((_, sep_pos)) => { + match (parser.method)(input, sep_pos) { + Ok((more_item, more_pos)) => { + items.push(more_item); + pos = more_pos; + } + Err(e) => { + // return early when there is an + // error matching the succeeding + // items + return Err(e); + } + } + } + Err(_e) => { + // the separator does not match, just break + break; + } + } + } + } + Err(e) => { + // return early when there is an error matching the first item + return Err(e); + } + } + Ok((items, pos)) + }) + } + + pub(super) fn alpha_or_underscore(ch: char) -> bool { + pom::char_class::alpha(ch as u8) || underscore(ch) + } + + pub(super) fn alphanum_or_underscore(ch: char) -> bool { + pom::char_class::alphanum(ch as u8) || underscore(ch) + } + + /// is this car is an underscore + pub(super) fn underscore(ch: char) -> bool { + ch == '_' + } + + pub fn new_line<'a>() -> Parser<'a, char, ()> { + one_of("\r\n").discard() + } + + /// any whitespace character + pub fn space<'a>() -> Parser<'a, char, ()> { + one_of(" \t").repeat(0..).discard() + } + + pub fn white_space<'a>() -> Parser<'a, char, ()> { + one_of(" \t\r\n").repeat(0..).discard() + } + + /// a valid identifier + pub(crate) fn ident<'a>() -> Parser<'a, char, String> { + (is_a(alpha_or_underscore) + is_a(alphanum_or_underscore).repeat(0..)) + .map(|(ch1, rest_ch)| { + format!("{}{}", ch1, String::from_iter(rest_ch)) + }) + } + + fn classes<'a>() -> Parser<'a, char, Vec<String>> { + list(ident(), sym(',')) + } + + fn tag_classes<'a>() -> Parser<'a, char, Vec<String>> { + sym('{') * classes() - sym('}') + } + + /// parse css tag in the format of '{', <identifier>, '}' + pub(crate) fn parse_css_tag( + input: &str, + ) -> Result<Vec<String>, pom::Error> { + let cell_text_chars: Vec<char> = input.chars().collect(); + parse_css_tag_chars(&cell_text_chars) + } + + fn parse_css_tag_chars(input: &[char]) -> Result<Vec<String>, pom::Error> { + tag_classes().parse(input) + } + + /// string inside a css content, taken as is as long as it is not `{` or `}` + fn css_strings<'a>() -> Parser<'a, char, String> { + let char_string = none_of("{}").repeat(1..).map(String::from_iter); + let string = char_string.repeat(0..); + string.map(|strings| strings.concat()) + } + + fn css_styles<'a>() -> Parser<'a, char, String> { + sym('{') * css_strings() - sym('}') + } + + /// a = {fill: red} + /// b = {stroke: blue} + fn css_style_list<'a>() -> Parser<'a, char, Vec<(String, String)>> { + list(class_and_style(), new_line()) + } + + /// a = {fill: red} + fn class_and_style<'a>() -> Parser<'a, char, (String, String)> { + (-space() * ident() - space() - sym('=') - space()) + css_styles() + } + + /// Parses: + /// #Legend:\n + /// a = {fill: red} + /// b = {stroke: blue} + /// + fn css_legend<'a>() -> Parser<'a, char, Vec<(String, String)>> { + (space() - sym('#') - space() - tag("Legend:") - space() - new_line()) + * css_style_list() + } + + fn css_legend_with_padding<'a>() -> Parser<'a, char, Vec<(String, String)>> + { + css_legend() - white_space() + } + + pub(crate) fn parse_css_legend( + input: &str, + ) -> Result<Vec<(String, String)>, pom::Error> { + let input_chars: Vec<char> = input.chars().collect(); + parse_css_legend_chars(&input_chars) + } + + fn parse_css_legend_chars( + input: &[char], + ) -> Result<Vec<(String, String)>, pom::Error> { + css_legend_with_padding().parse(input) + } + + fn escape_string<'a>() -> pom::parser::Parser<'a, char, (usize, usize)> { + let escape_sequence = sym('\\') * sym('"'); //escape sequence \" + let char_string = escape_sequence | none_of("\""); + let escaped_string_end = + sym('"') * char_string.repeat(0..).pos() - sym('"'); + none_of("\"").repeat(0..).pos() + escaped_string_end + - none_of("\"").repeat(0..).discard() + } + + pub(crate) fn line_parse<'a>( + ) -> pom::parser::Parser<'a, char, Vec<(usize, usize)>> { + escape_string().repeat(0..) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_escaped_string() { + let raw = + r#"The "qu/i/ck" brown "fox\"s" jumps over the lazy "do|g""#; + let input_chars: Vec<char> = raw.chars().collect(); + let char_locs = + line_parse().parse(&input_chars).expect("should parse"); + println!("output3: {:?}", char_locs); + let mut escaped = vec![]; + let mut cleaned = vec![]; + for (start, end) in char_locs.iter() { + escaped.push(&raw[*start..=*end]); + cleaned.push(&raw[*start + 1..*end]); + } + println!("{:#?}", escaped); + let expected = vec![r#""qu/i/ck""#, r#""fox\"s""#, r#""do|g""#]; + assert_eq!(expected, escaped); + let expected_cleaned = vec![r#"qu/i/ck"#, r#"fox\"s"#, r#"do|g"#]; + assert_eq!(expected_cleaned, cleaned); + } + + #[test] + fn test_css_styles() { + let input = "{fill:blue; stroke:red;}"; + let input_chars: Vec<char> = input.chars().collect(); + let css = css_styles().parse(&input_chars).expect("should parse"); + println!("css: {}", css); + assert_eq!(css, "fill:blue; stroke:red;"); + } + + #[test] + fn test_class_and_style() { + let input = "a = {fill:blue; stroke:red;}"; + let input_chars: Vec<char> = input.chars().collect(); + let css = + class_and_style().parse(&input_chars).expect("should parse"); + println!("css: {:?}", css); + assert_eq!( + css, + ("a".to_string(), "fill:blue; stroke:red;".to_string()) + ); + } + + #[test] + fn test_css_style_list() { + let input = "a = {fill:blue; stroke:red;}\n\ + b = {fill:red; stroke:black;}"; + let input_chars: Vec<char> = input.chars().collect(); + let css = + css_style_list().parse(&input_chars).expect("should parse"); + println!("css: {:?}", css); + assert_eq!( + css, + vec![ + ("a".to_string(), "fill:blue; stroke:red;".to_string()), + ("b".to_string(), "fill:red; stroke:black;".to_string()) + ] + ); + } + + #[test] + fn test_css_legend() { + let input = "# Legend: \n\ + a = {fill:blue; stroke:red;}\n\ + b = {fill:red; stroke:black;}"; + let input_chars: Vec<char> = input.chars().collect(); + let css = css_legend_with_padding() + .parse(&input_chars) + .expect("should parse"); + println!("css: {:?}", css); + assert_eq!( + css, + vec![ + ("a".to_string(), "fill:blue; stroke:red;".to_string()), + ("b".to_string(), "fill:red; stroke:black;".to_string()) + ] + ); + } + + #[test] + fn test_css_legend2() { + let input = "# Legend: \n\ + big_circle = {fill:blue;}\n\ + red = {stroke:red;}\n\n\n"; + let input_chars: Vec<char> = input.chars().collect(); + let css = css_legend_with_padding() + .parse(&input_chars) + .expect("should parse"); + println!("css: {:?}", css); + assert_eq!( + css, + vec![ + ("big_circle".to_string(), "fill:blue;".to_string()), + ("red".to_string(), "stroke:red;".to_string()) + ] + ); + } + #[test] + fn test_css_legend3() { + let input = "# Legend:\n\ + big_circle = {fill:blue;}\n\ + red = {stroke:red;}\n"; + let input_chars: Vec<char> = input.chars().collect(); + let css = css_legend_with_padding() + .parse(&input_chars) + .expect("should parse"); + println!("css: {:?}", css); + assert_eq!( + css, + vec![ + ("big_circle".to_string(), "fill:blue;".to_string()), + ("red".to_string(), "stroke:red;".to_string()) + ] + ); + } + + #[test] + #[should_panic] + fn test_css_legend_not_matching() { + let input = "# Legend1: \n\ + a = {fill:blue; stroke:red;}\n\ + b = {fill:red; stroke:black;}"; + let input_chars: Vec<char> = input.chars().collect(); + let css = css_legend().parse(&input_chars).expect("should parse"); + println!("css: {:?}", css); + assert_eq!( + css, + vec![ + ("a".to_string(), "fill:blue; stroke:red;".to_string()), + ("b".to_string(), "fill:red; stroke:black;".to_string()) + ] + ); + } + + #[test] + fn test_css_styles_with_new_line() { + let input = "{fill:blue;\n\ + stroke:red;}"; + let input_chars: Vec<char> = input.chars().collect(); + let css = css_styles().parse(&input_chars).expect("should parse"); + println!("css: {}", css); + assert_eq!( + css, + "fill:blue;\n\ + stroke:red;" + ); + } + + #[test] + fn test_tag_class() { + let input = "{blue_circle}"; + let input_chars: Vec<char> = input.chars().collect(); + let tag = tag_classes().parse(&input_chars).expect("should parse"); + assert_eq!(tag, vec!["blue_circle".to_string()]); + } + + #[test] + #[should_panic] + fn test_invalid_tag_class_not_closed() { + let input = "{blue_circle"; + let input_chars: Vec<char> = input.chars().collect(); + let tag = tag_classes().parse(&input_chars).expect("should parse"); + assert_eq!(tag, vec!["blue_circle".to_string()]); + } + + #[test] + #[should_panic] + fn test_invalid_tag_class_asterisk() { + let input = "{blue_*circle}"; + let input_chars: Vec<char> = input.chars().collect(); + let tag = tag_classes().parse(&input_chars).expect("should parse"); + assert_eq!(tag, vec!["blue_circle".to_string()]); + } + + #[test] + fn test_tag_classes() { + let input = "{blue_circle,red_dot}"; + let input_chars: Vec<char> = input.chars().collect(); + let tag = tag_classes().parse(&input_chars).expect("should parse"); + assert_eq!( + tag, + vec!["blue_circle".to_string(), "red_dot".to_string()] + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::buffer::CellGrid; + + #[test] + fn test_collinear() { + let a = CellGrid::a(); + let b = CellGrid::b(); + let c = CellGrid::c(); + let e = CellGrid::e(); + let f = CellGrid::f(); + assert!(is_collinear(&a, &b, &c)); + assert!(!is_collinear(&f, &c, &e)); + assert!(!is_collinear(&f, &a, &b)); + } + + #[test] + fn test_padding() { + assert_eq!(10.0, pad(9.1)); + assert_eq!(-2.0, pad(-1.21)); + assert_eq!(-3.0, pad(-2.99)); + } + + #[test] + fn test_clip_line() { + let a = CellGrid::a(); + let b = CellGrid::b(); + let bounds = AABB::new(*a, *b); + let clip = clip_line(&bounds, a, b); + println!("clip: {:#?}", clip); + assert_eq!(clip, Some((a, b))); + } + + #[test] + fn test_clip_line3() { + let c = CellGrid::c(); + let m = CellGrid::m(); + let w = CellGrid::w(); + let bounds = AABB::new(*m, *w); + let clip = clip_line(&bounds, c, w); + println!("clip: {:#?}", clip); + assert_eq!(clip, Some((m, w))); + } +} |