diff --git a/src/elements/connector.rs b/src/elements/connector.rs index 79edb64..11c57dc 100644 --- a/src/elements/connector.rs +++ b/src/elements/connector.rs @@ -1,15 +1,18 @@ use super::SvgElement; use crate::context::ElementMap; +use crate::elements::corner_route::render_match_corner; use crate::errors::{Result, SvgdxError}; -use crate::geometry::{parse_el_loc, strp_length, Length, LocSpec, ScalarSpec}; +use crate::geometry::{ + parse_el_loc, strp_length, BoundingBox, ElementLoc, Length, LocSpec, ScalarSpec, +}; use crate::types::{attr_split, fstr, strp}; pub fn is_connector(el: &SvgElement) -> bool { el.has_attr("start") && el.has_attr("end") && (el.name() == "line" || el.name() == "polyline") } -#[derive(Clone, Copy, Debug)] -enum Direction { +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Direction { Up, Right, Down, @@ -17,9 +20,9 @@ enum Direction { } #[derive(Clone, Copy, Debug)] -struct Endpoint { - origin: (f32, f32), - dir: Option, +pub struct Endpoint { + pub origin: (f32, f32), + pub dir: Option, } impl Endpoint { @@ -71,8 +74,8 @@ pub struct Connector { source_element: SvgElement, start_el: Option, end_el: Option, - start: Endpoint, - end: Endpoint, + pub start: Endpoint, + pub end: Endpoint, conn_type: ConnectionType, offset: Option, } @@ -123,6 +126,7 @@ fn shortest_link( for that_loc in edge_locations(conn_type) { let this_coord = this_bb.locspec(this_loc); let that_coord = that_bb.locspec(that_loc); + // always some as edge_locations does not include LineOffset let ((x1, y1), (x2, y2)) = (this_coord, that_coord); let dist_sq = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); if dist_sq < min_dist_sq { @@ -135,6 +139,12 @@ fn shortest_link( Ok((this_min_loc, that_min_loc)) } +#[allow(clippy::large_enum_variant)] +enum ElementParseData { + El(SvgElement, Option, Option), + Point(f32, f32), +} + impl Connector { fn loc_to_dir(loc: LocSpec) -> Option { match loc { @@ -146,18 +156,69 @@ impl Connector { } } + fn parse_element( + element: &mut SvgElement, + elem_map: &impl ElementMap, + attr_name: &str, + ) -> Result { + let this_ref = element + .pop_attr(attr_name) + .ok_or_else(|| SvgdxError::MissingAttribute(attr_name.to_string()))?; + + // Example: "#thing@tl" => top left coordinate of element id="thing" + if let Ok((elref, loc)) = parse_el_loc(&this_ref) { + let mut retdir = None; + let mut retloc = None; + if let Some(loc) = loc { + if let ElementLoc::LocSpec(ls) = loc { + retdir = Self::loc_to_dir(ls); + } + retloc = Some(loc); + } + Ok(ElementParseData::El( + elem_map + .get_element(&elref) + .ok_or(SvgdxError::ReferenceError(elref))? + .clone(), + retloc, + retdir, + )) + } else { + let mut parts = attr_split(&this_ref).map_while(|v| strp(&v).ok()); + + Ok(ElementParseData::Point( + parts.next().ok_or_else(|| { + SvgdxError::InvalidData( + (attr_name.to_owned() + "_ref x should be numeric").to_owned(), + ) + })?, + parts.next().ok_or_else(|| { + SvgdxError::InvalidData( + (attr_name.to_owned() + "_ref y should be numeric").to_owned(), + ) + })?, + )) + } + } + pub fn from_element( element: &SvgElement, elem_map: &impl ElementMap, conn_type: ConnectionType, ) -> Result { let mut element = element.clone(); - let start_ref = element - .pop_attr("start") - .ok_or_else(|| SvgdxError::MissingAttribute("start".to_string()))?; - let end_ref = element - .pop_attr("end") - .ok_or_else(|| SvgdxError::MissingAttribute("end".to_string()))?; + + let start_ret = Self::parse_element(&mut element, elem_map, "start")?; + let end_ret = Self::parse_element(&mut element, elem_map, "end")?; + let (start_el, mut start_loc, mut start_dir, start_point) = match start_ret { + ElementParseData::El(a, b, c) => (Some(a), b, c, None), + ElementParseData::Point(x, y) => (None, None, None, Some((x, y))), + }; + let (end_el, mut end_loc, mut end_dir, end_point) = match end_ret { + ElementParseData::El(a, b, c) => (Some(a), b, c, None), + ElementParseData::Point(x, y) => (None, None, None, Some((x, y))), + }; + let offset = if let Some(o_inner) = element.pop_attr("corner-offset") { Some( strp_length(&o_inner) @@ -171,50 +232,6 @@ impl Connector { // Needs to support explicit coordinate pairs or element references, and // for element references support given locations or not (in which case // the location is determined automatically to give the shortest distance) - let mut start_el: Option<&SvgElement> = None; - let mut end_el: Option<&SvgElement> = None; - let mut start_loc: Option = None; - let mut end_loc: Option = None; - let mut start_point: Option<(f32, f32)> = None; - let mut end_point: Option<(f32, f32)> = None; - let mut start_dir: Option = None; - let mut end_dir: Option = None; - - // Example: "#thing@tl" => top left coordinate of element id="thing" - if let Ok((elref, loc)) = parse_el_loc(&start_ref) { - if let Some(loc) = loc { - start_dir = Self::loc_to_dir(loc); - start_loc = Some(loc); - } - start_el = elem_map.get_element(&elref); - } else { - let mut parts = attr_split(&start_ref).map_while(|v| strp(&v).ok()); - start_point = Some(( - parts.next().ok_or_else(|| { - SvgdxError::InvalidData("start_ref x should be numeric".to_owned()) - })?, - parts.next().ok_or_else(|| { - SvgdxError::InvalidData("start_ref y should be numeric".to_owned()) - })?, - )); - } - if let Ok((elref, loc)) = parse_el_loc(&end_ref) { - if let Some(loc) = loc { - end_dir = Self::loc_to_dir(loc); - end_loc = Some(loc); - } - end_el = elem_map.get_element(&elref); - } else { - let mut parts = attr_split(&end_ref).map_while(|v| strp(&v).ok()); - end_point = Some(( - parts.next().ok_or_else(|| { - SvgdxError::InvalidData("end_ref x should be numeric".to_owned()) - })?, - parts.next().ok_or_else(|| { - SvgdxError::InvalidData("end_ref y should be numeric".to_owned()) - })?, - )); - } let (start, end) = match (start_point, end_point) { (Some(start_point), Some(end_point)) => ( @@ -222,17 +239,16 @@ impl Connector { Endpoint::new(end_point, end_dir), ), (Some(start_point), None) => { - let end_el = - end_el.ok_or_else(|| SvgdxError::InternalLogicError("no end_el".to_owned()))?; + let end_el = end_el + .as_ref() + .ok_or_else(|| SvgdxError::InternalLogicError("no end_el".to_owned()))?; if end_loc.is_none() { let eloc = closest_loc(end_el, start_point, conn_type, elem_map)?; - end_loc = Some(eloc); + end_loc = Some(ElementLoc::LocSpec(eloc)); end_dir = Self::loc_to_dir(eloc); } - let end_coord = elem_map - .get_element_bbox(end_el)? - .ok_or_else(|| SvgdxError::MissingBoundingBox(end_el.to_string()))? - .locspec(end_loc.expect("Set from closest_loc")); + let end_coord = end_el + .get_element_loc_coord(elem_map, end_loc.expect("Set from closest_loc"))?; ( Endpoint::new(start_point, start_dir), Endpoint::new(end_coord, end_dir), @@ -240,16 +256,15 @@ impl Connector { } (None, Some(end_point)) => { let start_el = start_el + .as_ref() .ok_or_else(|| SvgdxError::InternalLogicError("no start_el".to_owned()))?; if start_loc.is_none() { let sloc = closest_loc(start_el, end_point, conn_type, elem_map)?; - start_loc = Some(sloc); + start_loc = Some(ElementLoc::LocSpec(sloc)); start_dir = Self::loc_to_dir(sloc); } - let start_coord = elem_map - .get_element_bbox(start_el)? - .ok_or_else(|| SvgdxError::MissingBoundingBox(start_el.to_string()))? - .locspec(start_loc.expect("Set from closest_loc")); + let start_coord = start_el + .get_element_loc_coord(elem_map, start_loc.expect("Set from closest_loc"))?; ( Endpoint::new(start_coord, start_dir), Endpoint::new(end_point, end_dir), @@ -258,40 +273,35 @@ impl Connector { (None, None) => { let (start_el, end_el) = ( start_el + .as_ref() .ok_or_else(|| SvgdxError::InternalLogicError("no start_el".to_owned()))?, - end_el.ok_or_else(|| SvgdxError::InternalLogicError("no end_el".to_owned()))?, + end_el + .as_ref() + .ok_or_else(|| SvgdxError::InternalLogicError("no end_el".to_owned()))?, ); if start_loc.is_none() && end_loc.is_none() { let (sloc, eloc) = shortest_link(start_el, end_el, conn_type, elem_map)?; - start_loc = Some(sloc); - end_loc = Some(eloc); + start_loc = Some(ElementLoc::LocSpec(sloc)); + end_loc = Some(ElementLoc::LocSpec(eloc)); start_dir = Self::loc_to_dir(sloc); end_dir = Self::loc_to_dir(eloc); } else if start_loc.is_none() { - let end_coord = elem_map - .get_element_bbox(end_el)? - .ok_or_else(|| SvgdxError::MissingBoundingBox(end_el.to_string()))? - .locspec(end_loc.expect("Not both None")); + let end_coord = + end_el.get_element_loc_coord(elem_map, end_loc.expect("Not both None"))?; let sloc = closest_loc(start_el, end_coord, conn_type, elem_map)?; - start_loc = Some(sloc); + start_loc = Some(ElementLoc::LocSpec(sloc)); start_dir = Self::loc_to_dir(sloc); } else if end_loc.is_none() { - let start_coord = elem_map - .get_element_bbox(start_el)? - .ok_or_else(|| SvgdxError::MissingBoundingBox(start_el.to_string()))? - .locspec(start_loc.expect("Not both None")); + let start_coord = start_el + .get_element_loc_coord(elem_map, start_loc.expect("Not both None"))?; let eloc = closest_loc(end_el, start_coord, conn_type, elem_map)?; - end_loc = Some(eloc); + end_loc = Some(ElementLoc::LocSpec(eloc)); end_dir = Self::loc_to_dir(eloc); } - let start_coord = elem_map - .get_element_bbox(start_el)? - .ok_or_else(|| SvgdxError::MissingBoundingBox(start_el.to_string()))? - .locspec(start_loc.expect("Set above")); - let end_coord = elem_map - .get_element_bbox(end_el)? - .ok_or_else(|| SvgdxError::MissingBoundingBox(end_el.to_string()))? - .locspec(end_loc.expect("Set above")); + let start_coord = + start_el.get_element_loc_coord(elem_map, start_loc.expect("Set above"))?; + let end_coord = + end_el.get_element_loc_coord(elem_map, end_loc.expect("Set above"))?; ( Endpoint::new(start_coord, start_dir), Endpoint::new(end_coord, end_dir), @@ -302,8 +312,8 @@ impl Connector { source_element: element, start, end, - start_el: start_el.cloned(), - end_el: end_el.cloned(), + start_el, + end_el, conn_type, offset, }) @@ -394,96 +404,47 @@ impl Connector { ) .with_attrs_from(&self.source_element), ConnectionType::Corner => { - let points; - if let (Some(start_dir_some), Some(end_dir_some)) = (self.start.dir, self.end.dir) { - points = match (start_dir_some, end_dir_some) { - // L-shaped connection - (Direction::Up | Direction::Down, Direction::Left | Direction::Right) => { - vec![(x1, y1), (self.start.origin.0, self.end.origin.1), (x2, y2)] - } - (Direction::Left | Direction::Right, Direction::Up | Direction::Down) => { - vec![(x1, y1), (self.end.origin.0, self.start.origin.1), (x2, y2)] - } - // Z-shaped connection - (Direction::Left, Direction::Right) - | (Direction::Right, Direction::Left) => { - let mid_x = self - .offset - .unwrap_or(default_ratio_offset) - .calc_offset(self.start.origin.0, self.end.origin.0); - vec![(x1, y1), (mid_x, y1), (mid_x, y2), (x2, y2)] - } - (Direction::Up, Direction::Down) | (Direction::Down, Direction::Up) => { - let mid_y = self - .offset - .unwrap_or(default_ratio_offset) - .calc_offset(self.start.origin.1, self.end.origin.1); - vec![(x1, y1), (x1, mid_y), (x2, mid_y), (x2, y2)] - } - // U-shaped connection - (Direction::Left, Direction::Left) => { - let min_x = self.start.origin.0.min(self.end.origin.0); - let mid_x = min_x - - self - .offset - .unwrap_or(default_abs_offset) - .absolute() - .ok_or_else(|| { - SvgdxError::InvalidData( - "Corner type requires absolute offset".to_owned(), - ) - })?; - vec![(x1, y1), (mid_x, y1), (mid_x, y2), (x2, y2)] - } - (Direction::Right, Direction::Right) => { - let max_x = self.start.origin.0.max(self.end.origin.0); - let mid_x = max_x - + self - .offset - .unwrap_or(default_abs_offset) - .absolute() - .ok_or_else(|| { - SvgdxError::InvalidData( - "Corner type requires absolute offset".to_owned(), - ) - })?; - - vec![(x1, y1), (mid_x, y1), (mid_x, y2), (x2, y2)] - } - (Direction::Up, Direction::Up) => { - let min_y = self.start.origin.1.min(self.end.origin.1); - let mid_y = min_y - - self - .offset - .unwrap_or(default_abs_offset) - .absolute() - .ok_or_else(|| { - SvgdxError::InvalidData( - "Corner type requires absolute offset".to_owned(), - ) - })?; - - vec![(x1, y1), (x1, mid_y), (x2, mid_y), (x2, y2)] - } - (Direction::Down, Direction::Down) => { - let max_y = self.start.origin.1.max(self.end.origin.1); - let mid_y = max_y - + self - .offset - .unwrap_or(default_abs_offset) - .absolute() - .ok_or_else(|| { - SvgdxError::InvalidData( - "Corner type requires absolute offset".to_owned(), - ) - })?; + let mut abs_offset_set = false; + let mut start_abs_offset = default_abs_offset + .absolute() + .expect("set to absolute 3 above"); + let mut end_abs_offset = start_abs_offset; + let mut ratio_offset = default_ratio_offset + .ratio() + .expect("set to ratio 0.5 above"); + if let Some(offset) = &self.offset { + if let Some(o) = offset.absolute() { + start_abs_offset = o; + end_abs_offset = o; + abs_offset_set = true; + } + if let Some(r) = offset.ratio() { + ratio_offset = r; + } + } - vec![(x1, y1), (x1, mid_y), (x2, mid_y), (x2, y2)] - } - }; - } else { - points = vec![(x1, y1), (x2, y2)]; + let mut start_el_bb = BoundingBox::new(x1, y1, x1, y1); + let mut end_el_bb = BoundingBox::new(x2, y2, x2, y2); + if let Some(el) = &self.start_el { + if let Ok(Some(el_bb)) = el.bbox() { + start_el_bb = el_bb; + } + } + if let Some(el) = &self.end_el { + if let Ok(Some(el_bb)) = el.bbox() { + end_el_bb = el_bb; + } } + let points = render_match_corner( + self, + ratio_offset, + start_abs_offset, + end_abs_offset, + start_el_bb, + end_el_bb, + abs_offset_set, + )?; + // TODO: remove repeated points. if points.len() == 2 { SvgElement::new( diff --git a/src/elements/corner_route.rs b/src/elements/corner_route.rs new file mode 100644 index 0000000..73fd6ba --- /dev/null +++ b/src/elements/corner_route.rs @@ -0,0 +1,438 @@ +use crate::elements::connector::Connector; +use crate::errors::Result; +use std::cmp::Ordering; +use std::collections::BinaryHeap; + +use crate::{elements::connector::Direction, geometry::BoundingBox}; + +// from the example heap docs https://doc.rust-lang.org/std/collections/binary_heap/index.html +#[derive(PartialEq, Eq)] +struct PathCost { + cost: u32, + idx: usize, +} + +impl Ord for PathCost { + fn cmp(&self, other: &Self) -> Ordering { + // Notice that we flip the ordering on costs. + // In case of a tie we compare positions - this step is necessary + // to make implementations of `PartialEq` and `Ord` consistent. + other + .cost + .cmp(&self.cost) + .then_with(|| self.idx.cmp(&other.idx)) + } +} + +impl PartialOrd for PathCost { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// checks if there a axis aligned line segment is intersected by a bounding box +// this allows the aals to be entirely inside +fn aals_blocked_by_bb(bb: BoundingBox, a: f32, b: f32, x_axis: bool, axis_val: f32) -> bool { + if x_axis { + if axis_val < bb.y1 || axis_val > bb.y2 { + return false; + } + if (a < bb.x1) == (b < bb.x1) && (a > bb.x2) == (b > bb.x2) { + return false; + } + } else { + if axis_val < bb.x1 || axis_val > bb.x2 { + return false; + } + if (a < bb.y1) == (b < bb.y1) && (a > bb.y2) == (b > bb.y2) { + return false; + } + } + + true +} + +fn get_lines( + connector: &Connector, + ratio_offset: f32, + abs_offsets: (f32, f32), + bbs: (BoundingBox, BoundingBox), + abs_offset_set: bool, + dirs: (Direction, Direction), +) -> (Vec, Vec, usize, usize) { + let (x1, y1) = connector.start.origin; + let (x2, y2) = connector.end.origin; + let (start_el_bb, end_el_bb) = bbs; + let (start_abs_offset, end_abs_offset) = abs_offsets; + let (start_dir, end_dir) = dirs; + + let mut x_lines = vec![]; + let mut y_lines = vec![]; + let mut mid_x = usize::MAX; + let mut mid_y = usize::MAX; + + x_lines.push(start_el_bb.x1 - start_abs_offset); + x_lines.push(start_el_bb.x2 + start_abs_offset); + x_lines.push(end_el_bb.x1 - end_abs_offset); + x_lines.push(end_el_bb.x2 + end_abs_offset); + + if start_el_bb.x1 > end_el_bb.x2 { + // there is a gap + x_lines.push((start_el_bb.x1 + end_el_bb.x2) * 0.5); + mid_x = x_lines.len() - 1; + } else if start_el_bb.x2 < end_el_bb.x1 { + // there is a gap + x_lines.push((start_el_bb.x2 + end_el_bb.x1) * 0.5); + mid_x = x_lines.len() - 1; + } + + y_lines.push(start_el_bb.y1 - start_abs_offset); + y_lines.push(start_el_bb.y2 + start_abs_offset); + y_lines.push(end_el_bb.y1 - end_abs_offset); + y_lines.push(end_el_bb.y2 + end_abs_offset); + + if start_el_bb.y1 > end_el_bb.y2 { + // there is a gap + y_lines.push(start_el_bb.y1 * (1.0 - ratio_offset) + end_el_bb.y2 * ratio_offset); + mid_y = y_lines.len() - 1; + } else if start_el_bb.y2 < end_el_bb.y1 { + // there is a gap + y_lines.push(start_el_bb.y2 * (1.0 - ratio_offset) + end_el_bb.y1 * ratio_offset); + mid_y = y_lines.len() - 1; + } + + match start_dir { + Direction::Left | Direction::Right => { + y_lines.push(y1); + } + Direction::Down | Direction::Up => { + x_lines.push(x1); + } + } + + match end_dir { + Direction::Left | Direction::Right => { + y_lines.push(y2); + } + Direction::Down | Direction::Up => { + x_lines.push(x2); + } + } + + if abs_offset_set { + match start_dir { + Direction::Down => mid_y = 1, // positive y + Direction::Left => mid_x = 0, // negative x + Direction::Right => mid_x = 1, // positive x + Direction::Up => mid_y = 0, // negative y + } + } + + (x_lines, y_lines, mid_x, mid_y) +} + +fn get_edges( + point_set: &[(f32, f32)], + start_el_bb: BoundingBox, + end_el_bb: BoundingBox, +) -> Vec> { + let mut edge_set = vec![vec![]; point_set.len()]; + + for i in 0..point_set.len() { + for j in 0..point_set.len() { + if i == j { + continue; + } + let mut connected = false; + + // check if not blocked by a wall + if point_set[i].0 == point_set[j].0 + && !aals_blocked_by_bb( + start_el_bb, + point_set[i].1, + point_set[j].1, + false, + point_set[i].0, + ) + && !aals_blocked_by_bb( + end_el_bb, + point_set[i].1, + point_set[j].1, + false, + point_set[i].0, + ) + { + connected = true; + } + if point_set[i].1 == point_set[j].1 + && !aals_blocked_by_bb( + start_el_bb, + point_set[i].0, + point_set[j].0, + true, + point_set[i].1, + ) + && !aals_blocked_by_bb( + end_el_bb, + point_set[i].0, + point_set[j].0, + true, + point_set[i].1, + ) + { + connected = true; + } + if connected { + edge_set[i].push(j); + edge_set[j].push(i); + } + } + } + + edge_set +} + +fn add_start_and_end( + connector: &Connector, + point_set: &mut Vec<(f32, f32)>, + edge_set: &mut Vec>, + start_el_bb: BoundingBox, + end_el_bb: BoundingBox, + start_dir: Direction, + end_dir: Direction, +) -> (usize, usize) { + let (x1, y1) = connector.start.origin; + let (x2, y2) = connector.end.origin; + + point_set.push((x1, y1)); // start + point_set.push((x2, y2)); // end + edge_set.push(vec![]); + edge_set.push(vec![]); + + let start_ind = edge_set.len() - 2; + let end_ind = edge_set.len() - 1; + for i in 0..point_set.len() - 2 { + if point_set[i].0 == x1 + && ((point_set[i].1 < y1 && start_dir == Direction::Up) + || (point_set[i].1 > y1 && start_dir == Direction::Down)) + && !aals_blocked_by_bb(end_el_bb, point_set[i].1, y1, false, x1) + { + edge_set[i].push(start_ind); + edge_set[start_ind].push(i); + } + if point_set[i].1 == y1 + && ((point_set[i].0 > x1 && start_dir == Direction::Right) + || (point_set[i].0 < x1 && start_dir == Direction::Left)) + && !aals_blocked_by_bb(end_el_bb, point_set[i].0, x1, true, y1) + { + edge_set[i].push(start_ind); + edge_set[start_ind].push(i); + } + + if point_set[i].0 == x2 + && ((point_set[i].1 < y2 && end_dir == Direction::Up) + || (point_set[i].1 > y2 && end_dir == Direction::Down)) + && !aals_blocked_by_bb(start_el_bb, point_set[i].1, y2, false, x2) + { + edge_set[i].push(end_ind); + edge_set[end_ind].push(i); + } + if point_set[i].1 == y2 + && ((point_set[i].0 > x2 && end_dir == Direction::Right) + || (point_set[i].0 < x2 && end_dir == Direction::Left)) + && !aals_blocked_by_bb(start_el_bb, point_set[i].0, x2, true, y2) + { + edge_set[i].push(end_ind); + edge_set[end_ind].push(i); + } + } + + (start_ind, end_ind) +} + +fn cost_function( + point_set: &[(f32, f32)], + edge_set: &[Vec], + x_lines: Vec, + y_lines: Vec, + mid_x: usize, + mid_y: usize, + total_bb_size: u32, +) -> Vec> { + // edge cost function + + // needs to be comparable to or larger than total_bb_size + let corner_cost = 1 + total_bb_size; + let mut edge_costs = vec![vec![]; edge_set.len()]; + for i in 0..edge_set.len() { + for j in 0..edge_set[i].len() { + let ind_1 = i; + let ind_2 = edge_set[i][j]; + + let mid_point_mul_x = if mid_x != usize::MAX && point_set[ind_1].0 == x_lines[mid_x] { + 0.5 + } else { + 1.0 + }; + let mid_point_mul_y = if mid_y != usize::MAX && point_set[ind_1].1 == y_lines[mid_y] { + 0.5 + } else { + 1.0 + }; + + edge_costs[i].push( + ((point_set[ind_1].0 - point_set[ind_2].0).abs() * mid_point_mul_y + + (point_set[ind_1].1 - point_set[ind_2].1).abs() * mid_point_mul_x) + as u32 + + corner_cost, + ); // round may cause some problems + } + } + + edge_costs +} + +fn dijkstra_get_dists( + point_set: &[(f32, f32)], + edge_set: &[Vec], + edge_costs: &[Vec], + start_ind: usize, + end_ind: usize, +) -> Vec { + // just needs to be bigger than 5* (corner cost + total bounding box size) + let inf = u32::MAX; + let mut dist = vec![inf; point_set.len()]; + + let mut queue: BinaryHeap = BinaryHeap::new(); + dist[start_ind] = 0; + queue.push(PathCost { + cost: 0, + idx: start_ind, + }); + + // cant get stuck in a loop as cost for a distance either decreases or queue shrinks + while let Some(next) = queue.pop() { + if next.idx == end_ind { + break; + } + + // the node is reached by faster means so already popped + if next.cost > dist[next.idx] { + continue; + } + + for i in 0..edge_set[next.idx].len() { + let edge_cost = edge_costs[next.idx][i]; + if dist[next.idx] + edge_cost < dist[edge_set[next.idx][i]] { + dist[edge_set[next.idx][i]] = dist[next.idx] + edge_cost; + queue.push(PathCost { + cost: dist[edge_set[next.idx][i]], + idx: edge_set[next.idx][i], + }); + } + } + } + + dist +} + +fn dijkstra_get_points( + dist: Vec, + point_set: &[(f32, f32)], + edge_set: &[Vec], + edge_costs: &[Vec], + start_ind: usize, + end_ind: usize, +) -> Vec<(f32, f32)> { + let mut back_points_inds = vec![end_ind]; + let mut loc = end_ind; + while loc != start_ind { + // would get stuck in a loop if no valid solution + let mut quit = true; + for i in 0..edge_set[loc].len() { + if dist[edge_set[loc][i]] + edge_costs[loc][i] == dist[loc] { + loc = edge_set[loc][i]; + back_points_inds.push(loc); + quit = false; + break; + } + } + if quit { + break; + } + } + + let mut points = vec![]; + for i in (0..back_points_inds.len()).rev() { + points.push(point_set[back_points_inds[i]]); + } + points +} + +pub fn render_match_corner( + connector: &Connector, + ratio_offset: f32, + start_abs_offset: f32, + end_abs_offset: f32, + start_el_bb: BoundingBox, + end_el_bb: BoundingBox, + abs_offset_set: bool, +) -> Result> { + let (x1, y1) = connector.start.origin; + let (x2, y2) = connector.end.origin; + + // method generates all points it could possibly want to go through then does dijkstras on it + + let points: Vec<(f32, f32)>; + if let (Some(start_dir_some), Some(end_dir_some)) = (connector.start.dir, connector.end.dir) { + // x_lines have constant x vary over y + let (x_lines, y_lines, mid_x, mid_y) = get_lines( + connector, + ratio_offset, + (start_abs_offset, end_abs_offset), + (start_el_bb, end_el_bb), + abs_offset_set, + (start_dir_some, end_dir_some), + ); + let mut point_set = vec![]; + + for x in &x_lines { + for y in &y_lines { + point_set.push((*x, *y)); + } + } + + let mut edge_set = get_edges(&point_set, start_el_bb, end_el_bb); + let (start_ind, end_ind) = add_start_and_end( + connector, + &mut point_set, + &mut edge_set, + start_el_bb, + end_el_bb, + start_dir_some, + end_dir_some, + ); + + let total_bb = start_el_bb.combine(&end_el_bb); + let total_bb_size = + (total_bb.width() + total_bb.height() + start_abs_offset + end_abs_offset) as u32; + + let edge_costs = cost_function( + &point_set, + &edge_set, + x_lines, + y_lines, + mid_x, + mid_y, + total_bb_size, + ); + + let dist = dijkstra_get_dists(&point_set, &edge_set, &edge_costs, start_ind, end_ind); + + points = dijkstra_get_points(dist, &point_set, &edge_set, &edge_costs, start_ind, end_ind); + } else { + points = vec![(x1, y1), (x2, y2)]; + } + + Ok(points) +} diff --git a/src/elements/layout.rs b/src/elements/layout.rs index 8a89cc1..5fc6f09 100644 --- a/src/elements/layout.rs +++ b/src/elements/layout.rs @@ -7,11 +7,12 @@ use crate::constants::{ SCALARSPEC_SEP, VAR_PREFIX, }; use crate::context::{ContextView, ElementMap}; +use crate::elements::line_offset::get_point_along_linelike_type_el; use crate::elements::path::path_bbox; use crate::errors::{Result, SvgdxError}; use crate::geometry::{ - strp_length, BoundingBox, DirSpec, LocSpec, Position, ScalarSpec, Size, TransformAttr, - TrblLength, + strp_length, BoundingBox, DirSpec, ElementLoc, LocSpec, Position, ScalarSpec, Size, + TransformAttr, TrblLength, }; use crate::types::{attr_split, attr_split_cycle, extract_elref, fstr, split_compound_attr, strp}; @@ -226,6 +227,20 @@ impl SvgElement { Ok(()) } + pub fn get_element_loc_coord( + &self, + elem_map: &impl ElementMap, + loc: ElementLoc, + ) -> Result<(f32, f32)> { + match loc { + ElementLoc::LineOffset(l) => get_point_along_linelike_type_el(self, l), + ElementLoc::LocSpec(spec) => Ok(elem_map + .get_element_bbox(self)? + .ok_or_else(|| SvgdxError::MissingBoundingBox(self.to_string()))? + .locspec(spec)), + } + } + fn handle_containment(&mut self, ctx: &dyn ContextView) -> Result<()> { let (surround, inside) = (self.get_attr("surround"), self.get_attr("inside")); diff --git a/src/elements/line_offset.rs b/src/elements/line_offset.rs new file mode 100644 index 0000000..fee7bd3 --- /dev/null +++ b/src/elements/line_offset.rs @@ -0,0 +1,526 @@ +use super::SvgElement; +use crate::errors::{Result, SvgdxError}; +use crate::geometry::Length; +use crate::types::{attr_split, strp}; + +fn get_point_along_line(el: &SvgElement, length: Length) -> Result<(f32, f32)> { + let (is_percent, dist) = match length { + Length::Absolute(abs) => (false, abs), + Length::Ratio(ratio) => (true, ratio), + }; + + if let (Some(x1), Some(y1), Some(x2), Some(y2)) = ( + el.get_attr("x1"), + el.get_attr("y1"), + el.get_attr("x2"), + el.get_attr("y2"), + ) { + let x1: f32 = strp(x1)?; + let y1: f32 = strp(y1)?; + let x2: f32 = strp(x2)?; + let y2: f32 = strp(y2)?; + if x1 == x2 && y1 == y2 { + return Ok((x1, y1)); + } + + let ratio = if is_percent { + dist + } else { + let len = (x1 - x2).hypot(y1 - y2); + dist / len + }; + return Ok((x1 + ratio * (x2 - x1), y1 + ratio * (y2 - y1))); + } + + Err(SvgdxError::MissingAttribute( + "in line either x1, y1, x2 or y2".to_string(), + )) +} + +fn get_point_along_polyline(el: &SvgElement, length: Length) -> Result<(f32, f32)> { + let (is_percent, dist) = match length { + Length::Absolute(abs) => (false, abs), + Length::Ratio(ratio) => (true, ratio), + }; + + if let Some(points) = el.get_attr("points") { + let mut lastx = 0.0; + let mut lasty = 0.0; + + let mut points = attr_split(points); + let mut cumulative_dist = 0.0; + let mut first_point = true; + while let (Some(x), Some(y)) = (points.next(), points.next()) { + let x: f32 = strp(&x)?; + let y: f32 = strp(&y)?; + + if !first_point { + let len = (lastx - x).hypot(lasty - y); + if !is_percent && cumulative_dist + len > dist { + let ratio = (dist - cumulative_dist) / len; + return Ok(( + lastx * (1.0 - ratio) + ratio * x, + lasty * (1.0 - ratio) + ratio * y, + )); + } + cumulative_dist += len; + } else if dist < 0.0 { + // clamp to start + return Ok((x, y)); + } + lastx = x; + lasty = y; + + first_point = false; + } + if is_percent { + return get_point_along_polyline(el, Length::Absolute(dist * cumulative_dist)); + } + return Ok((lastx, lasty)); + } + + Err(SvgdxError::MissingAttribute( + "points in polyline".to_string(), + )) +} + +fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { + let (is_percent, dist) = match length { + Length::Absolute(abs) => (false, abs), + Length::Ratio(ratio) => (true, ratio), + }; + + if let Some(d) = el.get_attr("d") { + let mut pos = (0.0, 0.0); + + let mut cumulative_dist = 0.0; + let mut last_stable_pos = pos; + let mut r = 0.0; + let mut large_arc_flag = false; + let mut sweeping_flag = false; + + let mut op = ' '; + let mut arg_num = 0; + for item in attr_split(d) { + if item.starts_with([ + 'a', 'A', 'b', 'B', 'c', 'C', 'h', 'H', 'l', 'L', 'm', 'M', 'q', 'Q', 's', 'S', + 't', 'T', 'v', 'V', 'z', 'Z', + ]) { + if let Some(c) = item.chars().next() { + op = c; + arg_num = 0; + if dist < 0.0 && !['m', 'M'].contains(&c) { + // clamping the start + return Ok(last_stable_pos); + } + } + } else { + let num_args = match op { + 'm' | 'M' | 'l' | 'L' => 2, + 'h' | 'H' | 'v' | 'V' => 1, + 'a' | 'A' => 7, + _ => { + return Err(SvgdxError::InvalidData(format!( + "not yet impl path parsing line offset for {op}" + ))); + } + }; + if arg_num == num_args { + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + + match op { + 'm' | 'M' => { + let val = strp(&item)?; + let pos_ref = if arg_num == 0 { &mut pos.0 } else { &mut pos.1 }; + if op == 'm' { + *pos_ref += val; + } else { + *pos_ref = val; + } + } + 'h' | 'H' | 'v' | 'V' => { + let val = strp(&item)?; + let pos_ref = if op == 'h' || op == 'H' { + &mut pos.0 + } else { + &mut pos.1 + }; + if op == 'h' || op == 'v' { + *pos_ref += val; + } else { + *pos_ref = val; + } + let d = + (pos.0 - last_stable_pos.0).abs() + (pos.1 - last_stable_pos.1).abs(); + if !is_percent && cumulative_dist + d > dist { + let r = (dist - cumulative_dist) / d; + return Ok(( + last_stable_pos.0 * (1.0 - r) + pos.0 * r, + last_stable_pos.1 * (1.0 - r) + pos.1 * r, + )); + } + + cumulative_dist += d; + } + 'l' | 'L' => { + let val = strp(&item)?; + if arg_num == 0 { + if op == 'l' { + pos.0 += val; + } else { + pos.0 = val; + } + } else { + if op == 'l' { + pos.1 += val; + } else { + pos.1 = val; + } + let d = (last_stable_pos.0 - pos.0).hypot(last_stable_pos.1 - pos.1); + if !is_percent && cumulative_dist + d > dist { + let r = (dist - cumulative_dist) / d; + return Ok(( + last_stable_pos.0 * (1.0 - r) + pos.0 * r, + last_stable_pos.1 * (1.0 - r) + pos.1 * r, + )); + } + + cumulative_dist += d; + } + } + 'a' | 'A' => { + if arg_num == 0 { + let val = strp(&item)?; + r = val; + } else if arg_num == 1 { + let val = strp(&item)?; + if r != val { + return Err(SvgdxError::ParseError( + "path length not supported for non circle elipse".to_string(), + )); + } + } else if arg_num == 2 { + // unused as not mean anything for circle + } else if arg_num == 3 { + let val = item.parse::()?; + large_arc_flag = val != 0; + } else if arg_num == 4 { + let val = item.parse::()?; + sweeping_flag = val != 0; + } else if arg_num == 5 { + let val = strp(&item)?; + if op == 'a' { + pos.0 += val; + } else { + pos.0 = val; + } + } else { + let val = strp(&item)?; + if op == 'a' { + pos.1 += val; + } else { + pos.1 = val; + } + + let d2 = (last_stable_pos.0 - pos.0) * (last_stable_pos.0 - pos.0) + + (last_stable_pos.1 - pos.1) * (last_stable_pos.1 - pos.1); + let d = d2.sqrt(); + + let desc = r * r - d2 / 4.0; + let mid_point = ( + (last_stable_pos.0 + pos.0) * 0.5, + (last_stable_pos.1 + pos.1) * 0.5, + ); + let centre = if desc <= 0.0 { + r = d / 2.0; + mid_point + } else { + let inv_d = 1.0 / d; + let perp = ( + (last_stable_pos.1 - pos.1) * inv_d, + (pos.0 - last_stable_pos.0) * inv_d, + ); + let sign = large_arc_flag ^ sweeping_flag; // which circle to use + let len = if sign { desc.sqrt() } else { -desc.sqrt() }; + + (mid_point.0 + perp.0 * len, mid_point.1 + perp.1 * len) + }; + let ang_1 = + (last_stable_pos.1 - centre.1).atan2(last_stable_pos.0 - centre.0); + let ang_2 = (pos.1 - centre.1).atan2(pos.0 - centre.0); + + let mut shortest_arc_angle = ang_2 - ang_1; + if shortest_arc_angle < -std::f32::consts::PI { + shortest_arc_angle += std::f32::consts::PI * 2.0; + } else if shortest_arc_angle > std::f32::consts::PI { + shortest_arc_angle -= std::f32::consts::PI * 2.0; + } + + if (shortest_arc_angle.abs() - std::f32::consts::PI).abs() < 0.0001 + && (shortest_arc_angle > 0.0) ^ !sweeping_flag + { + shortest_arc_angle = -shortest_arc_angle; + } + + let arc_angle = if large_arc_flag { + (std::f32::consts::PI * 2.0 - shortest_arc_angle.abs()) + * shortest_arc_angle.signum() + } else { + shortest_arc_angle + }; + let arc_length = arc_angle.abs() * r; + + if !is_percent && cumulative_dist + arc_length > dist { + let ratio = (dist - cumulative_dist) / arc_length; + let final_angle = ang_1 + arc_angle * ratio; + + return Ok(( + centre.0 + r * (final_angle).cos(), + centre.1 + r * (final_angle).sin(), + )); + } + + cumulative_dist += arc_length; + } + } + _ => { + return Err(SvgdxError::InvalidData(format!( + "not yet impl path parsing line offset for {op}" + ))); + } + } + + arg_num += 1; + + if arg_num == num_args { + last_stable_pos = pos; + } + } + } + if is_percent { + return get_point_along_path(el, Length::Absolute(dist * cumulative_dist)); + } + return Ok(pos); + } + + Err(SvgdxError::MissingAttribute("d in path".to_string())) +} + +pub fn get_point_along_linelike_type_el(el: &SvgElement, length: Length) -> Result<(f32, f32)> { + match el.name() { + "line" => get_point_along_line(el, length), + "polyline" => get_point_along_polyline(el, length), + "path" => get_point_along_path(el, length), + _ => Err(SvgdxError::InternalLogicError( + "looking for point on line in a non line element".to_string(), + )), + } +} + +#[cfg(test)] +mod tests { + + use assertables::assert_abs_diff_le_x; + + use crate::{ + elements::{line_offset::get_point_along_linelike_type_el, SvgElement}, + geometry::Length, + }; + + #[test] + fn test_line() { + let element = SvgElement::new( + "line", + &[ + ("x1".to_string(), "1".to_string()), + ("y1".to_string(), "0.5".to_string()), + ("x2".to_string(), "1.75".to_string()), + ("y2".to_string(), "-0.5".to_string()), + ], + ); + + // test absolute + let result = get_point_along_linelike_type_el(&element, Length::Absolute(1.0)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 1.6, 0.001); + assert_abs_diff_le_x!(y, -0.3, 0.001); + + // test ratio + let result = get_point_along_linelike_type_el(&element, Length::Ratio(0.6)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 1.45, 0.001); + assert_abs_diff_le_x!(y, -0.1, 0.001); + + // negative abs + let result = get_point_along_linelike_type_el(&element, Length::Absolute(-0.5)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 0.7, 0.001); + assert_abs_diff_le_x!(y, 0.9, 0.001); + + // too big ratio + let result = get_point_along_linelike_type_el(&element, Length::Ratio(1.8)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 2.35, 0.001); + assert_abs_diff_le_x!(y, -1.3, 0.001); + } + + #[test] + fn test_polyline() { + let element = SvgElement::new( + "polyline", + &[("points".to_string(), "1 2 4 6 -8 1 -1 1".to_string())], + ); + // length is 5 + 13 + 7 = 25 + + // test absolute in first section + let result = get_point_along_linelike_type_el(&element, Length::Absolute(1.0)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 1.6, 0.001); + assert_abs_diff_le_x!(y, 2.8, 0.001); + + // test ratio in first section + let result = get_point_along_linelike_type_el(&element, Length::Ratio(0.1)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 2.5, 0.001); + assert_abs_diff_le_x!(y, 4.0, 0.001); + + // test abs + let result = get_point_along_linelike_type_el(&element, Length::Absolute(11.5)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, -2.0, 0.001); + assert_abs_diff_le_x!(y, 3.5, 0.001); + + // test ratio + let result = get_point_along_linelike_type_el(&element, Length::Ratio(0.85)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, -4.75, 0.001); + assert_abs_diff_le_x!(y, 1.0, 0.001); + + // test abs negative + let result = get_point_along_linelike_type_el(&element, Length::Absolute(-0.85)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 1.0, 0.001); + assert_abs_diff_le_x!(y, 2.0, 0.001); + + // test ratio large + let result = get_point_along_linelike_type_el(&element, Length::Ratio(2.85)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, -1.0, 0.001); + assert_abs_diff_le_x!(y, 1.0, 0.001); + } + + #[test] + fn test_path() { + // test empty d + let element = SvgElement::new("path", &[("d".to_string(), "".to_string())]); + let result = get_point_along_linelike_type_el(&element, Length::Absolute(1.5)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 0.0, 0.001); + assert_abs_diff_le_x!(y, 0.0, 0.001); + + // test only M + let element = SvgElement::new("path", &[("d".to_string(), "M 1 2".to_string())]); + let result = get_point_along_linelike_type_el(&element, Length::Absolute(1.5)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 1.0, 0.001); + assert_abs_diff_le_x!(y, 2.0, 0.001); + + // test h + let element = SvgElement::new("path", &[("d".to_string(), "M 1 2 h 4".to_string())]); + let result = get_point_along_linelike_type_el(&element, Length::Ratio(0.75)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 4.0, 0.001); + assert_abs_diff_le_x!(y, 2.0, 0.001); + + // test v + let element = SvgElement::new("path", &[("d".to_string(), "M 1 2 v 4".to_string())]); + let result = get_point_along_linelike_type_el(&element, Length::Ratio(0.75)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 1.0, 0.001); + assert_abs_diff_le_x!(y, 5.0, 0.001); + + // test H + let element = SvgElement::new("path", &[("d".to_string(), "M 1 2 H 4".to_string())]); + let result = get_point_along_linelike_type_el(&element, Length::Ratio(0.75)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 3.25, 0.001); + assert_abs_diff_le_x!(y, 2.0, 0.001); + + // test V + let element = SvgElement::new("path", &[("d".to_string(), "M 1 2 V 4".to_string())]); + let result = get_point_along_linelike_type_el(&element, Length::Ratio(0.75)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 1.0, 0.001); + assert_abs_diff_le_x!(y, 3.5, 0.001); + + // test l + let element = SvgElement::new("path", &[("d".to_string(), "M 1 2 l 3 4".to_string())]); + let result = get_point_along_linelike_type_el(&element, Length::Absolute(3.0)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 2.8, 0.001); + assert_abs_diff_le_x!(y, 4.4, 0.001); + + // test L + let element = SvgElement::new("path", &[("d".to_string(), "M 1 2 L 7 10".to_string())]); + let result = get_point_along_linelike_type_el(&element, Length::Ratio(0.7)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 5.2, 0.001); + assert_abs_diff_le_x!(y, 7.6, 0.001); + + // test a + + // over extended + let element = SvgElement::new( + "path", + &[("d".to_string(), "M 3 3 a 3 3 45 0 0 -6 -6".to_string())], + ); + let result = get_point_along_linelike_type_el(&element, Length::Ratio(0.5)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, -3.0, 0.001); + assert_abs_diff_le_x!(y, 3.0, 0.001); + + // over extended other direction + let element = SvgElement::new( + "path", + &[("d".to_string(), "M 1 2 a 3 3 45 0 1 -6 -6".to_string())], + ); + let result = get_point_along_linelike_type_el(&element, Length::Ratio(0.5)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 1.0, 0.001); + assert_abs_diff_le_x!(y, -4.0, 0.001); + + // test A + + // test not over extended + let element = SvgElement::new( + "path", + &[("d".to_string(), "M 3 0 A 3 3 45 0 1 0 3".to_string())], + ); + let result = get_point_along_linelike_type_el(&element, Length::Ratio(0.5)); + assert!(result.is_ok()); + let (x, y) = result.unwrap(); + assert_abs_diff_le_x!(x, 1.5 * 2.0f32.sqrt(), 0.001); + assert_abs_diff_le_x!(y, 1.5 * 2.0f32.sqrt(), 0.001); + } +} diff --git a/src/elements/mod.rs b/src/elements/mod.rs index 5dbe0cf..3d4beb3 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -1,8 +1,10 @@ mod bearing; mod connector; mod containers; +mod corner_route; mod element; mod layout; +mod line_offset; mod loops; mod path; mod reuse; diff --git a/src/elements/text.rs b/src/elements/text.rs index ef8c4da..9854321 100644 --- a/src/elements/text.rs +++ b/src/elements/text.rs @@ -144,9 +144,9 @@ fn get_text_position(element: &mut SvgElement) -> Result<(f32, f32, bool, LocSpe .bbox()? .ok_or_else(|| SvgdxError::MissingBoundingBox(element.to_string()))? .locspec(text_anchor); + tdx += t_dx; tdy += t_dy; - Ok((tdx, tdy, outside, text_anchor, text_classes)) } diff --git a/src/geometry/mod.rs b/src/geometry/mod.rs index 4f0fde1..9fb4323 100644 --- a/src/geometry/mod.rs +++ b/src/geometry/mod.rs @@ -7,6 +7,6 @@ pub use bbox::{BoundingBox, BoundingBoxBuilder}; pub use position::Position; pub use transform_attr::TransformAttr; pub use types::{ - parse_el_loc, parse_el_scalar, strp_length, DirSpec, Length, LocSpec, ScalarSpec, Size, - TrblLength, + parse_el_loc, parse_el_scalar, strp_length, DirSpec, ElementLoc, Length, LocSpec, ScalarSpec, + Size, TrblLength, }; diff --git a/src/geometry/types.rs b/src/geometry/types.rs index e724739..b3fd960 100644 --- a/src/geometry/types.rs +++ b/src/geometry/types.rs @@ -141,6 +141,13 @@ pub enum LocSpec { LeftEdge(Length), } +/// `LocSpec` defines a location on a `SvgElement` +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ElementLoc { + LocSpec(LocSpec), + LineOffset(Length), +} + impl LocSpec { pub fn is_top(&self) -> bool { matches!( @@ -171,6 +178,28 @@ impl LocSpec { } } +impl FromStr for ElementLoc { + type Err = SvgdxError; + + fn from_str(value: &str) -> Result { + if let Ok(ls) = LocSpec::from_str(value) { + Ok(ElementLoc::LocSpec(ls)) + } else if let Some((edge, len)) = value.split_once(EDGESPEC_SEP) { + let len = len.parse::()?; + match edge { + "" => Ok(ElementLoc::LineOffset(len)), + _ => Err(SvgdxError::InvalidData(format!( + "Invalid LocSpec format {value}" + ))), + } + } else { + Err(SvgdxError::InvalidData(format!( + "Invalid LocSpec format {value}" + ))) + } + } +} + impl FromStr for LocSpec { type Err = SvgdxError; @@ -340,8 +369,8 @@ impl FromStr for DirSpec { } } -/// Parse a elref + optional locspec, e.g. `#id@tl:10%` or `#id` -pub fn parse_el_loc(s: &str) -> Result<(ElRef, Option)> { +/// Parse a elref + optional ElementLoc, e.g. `#id@tl:10%` or `#id` +pub fn parse_el_loc(s: &str) -> Result<(ElRef, Option)> { let (elref, remain) = extract_elref(s)?; if remain.is_empty() { return Ok((elref, None)); @@ -392,17 +421,23 @@ mod test { fn test_parse_loc() { assert_eq!( parse_el_loc("#a@b").unwrap(), - (ElRef::Id("a".to_string()), Some(LocSpec::Bottom)) + ( + ElRef::Id("a".to_string()), + Some(ElementLoc::LocSpec(LocSpec::Bottom)) + ) ); assert_eq!( parse_el_loc("#id@tl").unwrap(), - (ElRef::Id("id".to_string()), Some(LocSpec::TopLeft)) + ( + ElRef::Id("id".to_string()), + Some(ElementLoc::LocSpec(LocSpec::TopLeft)) + ) ); assert_eq!( parse_el_loc("#id@t:25%").unwrap(), ( ElRef::Id("id".to_string()), - Some(LocSpec::TopEdge(Length::Ratio(0.25))) + Some(ElementLoc::LocSpec(LocSpec::TopEdge(Length::Ratio(0.25)))) ) ); assert_eq!( diff --git a/tests/integration_tests/connector.rs b/tests/integration_tests/connector.rs index f710341..ea557f5 100644 --- a/tests/integration_tests/connector.rs +++ b/tests/integration_tests/connector.rs @@ -272,3 +272,18 @@ fn test_connector_previous() { let output = transform_str_default(input).unwrap(); assert_contains!(output, expected); } + +#[test] +fn test_connector_corners() { + let input = r##" + + + + +"##; + let expected1 = r##"id="c1" points="5 0, 5 -3, 33 -3, 33 25, 30 25""##; + let expected2 = r##"id="c2" points="0 5, -1 5, -1 15, 31 15, 31 24, 30 24""##; + let output = transform_str_default(input).unwrap(); + assert_contains!(output, expected1); + assert_contains!(output, expected2); +}