From cb9f5b1e0893a26f572c4d6164b3af00d77b6325 Mon Sep 17 00:00:00 2001 From: megamaths Date: Tue, 29 Jul 2025 12:10:34 +0100 Subject: [PATCH 1/8] connector rework to allow more corners --- src/elements/connector.rs | 562 ++++++++++++++++++++++++++++---------- 1 file changed, 421 insertions(+), 141 deletions(-) diff --git a/src/elements/connector.rs b/src/elements/connector.rs index 79edb64..42e56c6 100644 --- a/src/elements/connector.rs +++ b/src/elements/connector.rs @@ -1,14 +1,17 @@ +use std::cmp::Ordering; +use std::collections::BinaryHeap; + use super::SvgElement; use crate::context::ElementMap; 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, 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)] +#[derive(Clone, Copy, Debug, PartialEq)] enum Direction { Up, Right, @@ -135,6 +138,31 @@ fn shortest_link( Ok((this_min_loc, that_min_loc)) } +#[derive(PartialEq, Eq)] +struct HeapData { + cost: u32, + ind: usize, +} + +impl Ord for HeapData { + // from the exaple heap docs + 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.ind.cmp(&other.ind)) + } +} + +impl PartialOrd for HeapData { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl Connector { fn loc_to_dir(loc: LocSpec) -> Option { match loc { @@ -146,18 +174,64 @@ impl Connector { } } + fn parse_element<'a>( + element: &mut SvgElement, + elem_map: &'a impl ElementMap, + start: bool, + ) -> Result<( + Option<&'a SvgElement>, + Option, + Option<(f32, f32)>, + Option, + )> { + let attrib_name = if start { "start" } else { "end" }; + let this_ref = element + .pop_attr(attrib_name) + .ok_or_else(|| SvgdxError::MissingAttribute(attrib_name.to_string()))?; + + let mut t_el: Option<&SvgElement> = None; + let mut t_loc: Option = None; + let mut t_point: Option<(f32, f32)> = None; + let mut t_dir: Option = None; + + // Example: "#thing@tl" => top left coordinate of element id="thing" + if let Ok((elref, loc)) = parse_el_loc(&this_ref) { + if let Some(loc) = loc { + t_dir = Self::loc_to_dir(loc); + t_loc = Some(loc); + } + t_el = elem_map.get_element(&elref); + } else { + let mut parts = attr_split(&this_ref).map_while(|v| strp(&v).ok()); + t_point = Some(( + parts.next().ok_or_else(|| { + SvgdxError::InvalidData( + (attrib_name.to_owned() + "_ref x should be numeric").to_owned(), + ) + })?, + parts.next().ok_or_else(|| { + SvgdxError::InvalidData( + (attrib_name.to_owned() + "_ref y should be numeric").to_owned(), + ) + })?, + )); + } + + return Ok((t_el, t_loc, t_point, t_dir)); + } + 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_el, mut start_loc, start_point, mut start_dir) = + Self::parse_element(&mut element, elem_map, true)?; + let (end_el, mut end_loc, end_point, mut end_dir) = + Self::parse_element(&mut element, elem_map, false)?; + let offset = if let Some(o_inner) = element.pop_attr("corner-offset") { Some( strp_length(&o_inner) @@ -171,50 +245,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)) => ( @@ -309,6 +339,310 @@ impl Connector { }) } + 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; + } + } + + return true; + } + + fn render_match_conner( + &self, + ratio_offset: f32, + start_abs_offset: f32, + end_abs_offset: f32, + sel_bb: BoundingBox, + eel_bb: BoundingBox, + abs_offset_set: bool, + ) -> Result> { + let (x1, y1) = self.start.origin; + let (x2, y2) = self.end.origin; + + // method generates all points it could possibly want to go through then does dijkstras on it + + let mut points: Vec<(f32, f32)>; + if let (Some(start_dir_some), Some(end_dir_some)) = (self.start.dir, self.end.dir) { + points = vec![]; + + // x_lines have constant x vary over y + let mut x_lines = vec![]; + let mut y_lines = vec![]; + let mut point_set = vec![]; + let mut mid_x = std::usize::MAX; + let mut mid_y = std::usize::MAX; + + x_lines.push(sel_bb.x1 - start_abs_offset); + x_lines.push(sel_bb.x2 + start_abs_offset); + x_lines.push(eel_bb.x1 - end_abs_offset); + x_lines.push(eel_bb.x2 + end_abs_offset); + + if sel_bb.x1 > eel_bb.x2 { + // there is a gap + x_lines.push((sel_bb.x1 + eel_bb.x2) * 0.5); + mid_x = x_lines.len() - 1; + } else if sel_bb.x2 < eel_bb.x1 { + // there is a gap + x_lines.push((sel_bb.x2 + eel_bb.x1) * 0.5); + mid_x = x_lines.len() - 1; + } + + y_lines.push(sel_bb.y1 - start_abs_offset); + y_lines.push(sel_bb.y2 + start_abs_offset); + y_lines.push(eel_bb.y1 - end_abs_offset); + y_lines.push(eel_bb.y2 + end_abs_offset); + + if sel_bb.y1 > eel_bb.y2 { + // there is a gap + y_lines.push(sel_bb.y1 * (1.0 - ratio_offset) + eel_bb.y2 * ratio_offset); + mid_y = y_lines.len() - 1; + } else if sel_bb.y2 < eel_bb.y1 { + // there is a gap + y_lines.push(sel_bb.y2 * (1.0 - ratio_offset) + eel_bb.y1 * ratio_offset); + mid_y = y_lines.len() - 1; + } + + match start_dir_some { + Direction::Left | Direction::Right => { + y_lines.push(y1); + } + Direction::Down | Direction::Up => { + x_lines.push(x1); + } + } + + match end_dir_some { + Direction::Left | Direction::Right => { + y_lines.push(y2); + } + Direction::Down | Direction::Up => { + x_lines.push(x2); + } + } + + if abs_offset_set { + match start_dir_some { + Direction::Down => mid_y = 1, // positive x + Direction::Left => mid_x = 0, // negative y + Direction::Right => mid_x = 1, // positive y + Direction::Up => mid_y = 0, // positive x + } + } + + for i in 0..x_lines.len() { + for j in 0..y_lines.len() { + point_set.push((x_lines[i], y_lines[j])); + } + } + + 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 { + if !Self::aals_blocked_by_bb( + sel_bb, + point_set[i].1, + point_set[j].1, + false, + point_set[i].0, + ) && !Self::aals_blocked_by_bb( + eel_bb, + point_set[i].1, + point_set[j].1, + false, + point_set[i].0, + ) { + connected = true; + } + } else if point_set[i].1 == point_set[j].1 { + if !Self::aals_blocked_by_bb( + sel_bb, + point_set[i].0, + point_set[j].0, + true, + point_set[i].1, + ) && !Self::aals_blocked_by_bb( + eel_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); + } + } + } + + // just needs to be bigger than 5* (corner cost + total bounding box size) + let inf = 1000000; + + point_set.push((x1, y1)); // start + point_set.push((x2, y2)); // end + edge_set.push(vec![]); + edge_set.push(vec![]); + let mut queue: BinaryHeap = BinaryHeap::new(); + let mut dist = vec![inf; point_set.len()]; + + 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_some == Direction::Up) + || (point_set[i].0 == x1 + && point_set[i].1 > y1 + && start_dir_some == Direction::Down) + { + if !Self::aals_blocked_by_bb(eel_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_some == Direction::Right) + || (point_set[i].1 == y1 + && point_set[i].0 < x1 + && start_dir_some == Direction::Left) + { + if !Self::aals_blocked_by_bb(eel_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_some == Direction::Up) + || (point_set[i].0 == x2 + && point_set[i].1 > y2 + && end_dir_some == Direction::Down) + { + if !Self::aals_blocked_by_bb(sel_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_some == Direction::Right) + || (point_set[i].1 == y2 + && point_set[i].0 < x2 + && end_dir_some == Direction::Left) + { + if !Self::aals_blocked_by_bb(sel_bb, point_set[i].0, x2, true, y2) { + edge_set[i].push(end_ind); + edge_set[end_ind].push(i); + } + } + } + + // edge cost function + let corner_cost = 1000; + 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 != std::usize::MAX && point_set[ind_1].0 == x_lines[mid_x] { + 0.5 + } else { + 1.0 + }; + let mid_point_mul_y = + if mid_y != std::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 + } + } + + dist[start_ind] = 0; + queue.push(HeapData { + cost: 0, + ind: start_ind, + }); + + // cant get stuck in a loop as cost for a distance either decreases or queue shrinks + while !queue.is_empty() { + let next = queue.pop().expect("would not be in while loop"); + if next.ind == end_ind { + break; + } + + // the node is reached by faster means so already popped + if next.cost > dist[next.ind] { + continue; + } + + for i in 0..edge_set[next.ind].len() { + let edge_cost = edge_costs[next.ind][i]; + if dist[next.ind] + edge_cost < dist[edge_set[next.ind][i]] { + dist[edge_set[next.ind][i]] = dist[next.ind] + edge_cost; + queue.push(HeapData { + cost: dist[edge_set[next.ind][i]], + ind: edge_set[next.ind][i], + }); + } + } + } + + 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; + } + } + + for i in (0..back_points_inds.len()).rev() { + points.push(point_set[back_points_inds[i]]); + } + } else { + points = vec![(x1, y1), (x2, y2)]; + } + + return Ok(points); + } + pub fn render(&self, ctx: &impl ElementMap) -> Result { let default_ratio_offset = Length::Ratio(0.5); let default_abs_offset = Length::Absolute(3.); @@ -394,96 +728,42 @@ 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(), - ) - })?; - - vec![(x1, y1), (x1, mid_y), (x2, mid_y), (x2, y2)] - } - }; - } else { - points = vec![(x1, y1), (x2, y2)]; + let mut abs_offset_set = false; + let mut start_abs_offset = default_abs_offset.absolute().ok_or("blarg 13872199")?; + let mut end_abs_offset = start_abs_offset; + let mut ratio_offset = default_ratio_offset.ratio().ok_or("blarg 13872198")?; + 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; + } } + + let mut sel_bb = BoundingBox::new(x1, y1, x1, y1); + let mut eel_bb = BoundingBox::new(x2, y2, x2, y2); + if let Some(el) = &self.start_el { + if let Ok(Some(el_bb)) = el.bbox() { + sel_bb = el_bb; + } + } + if let Some(el) = &self.end_el { + if let Ok(Some(el_bb)) = el.bbox() { + eel_bb = el_bb; + } + } + let points = self.render_match_conner( + ratio_offset, + start_abs_offset, + end_abs_offset, + sel_bb, + eel_bb, + abs_offset_set, + )?; + // TODO: remove repeated points. if points.len() == 2 { SvgElement::new( From 778da90a5073ebab7285be50d5b2c4080f66e9ec Mon Sep 17 00:00:00 2001 From: megamaths Date: Mon, 4 Aug 2025 12:44:51 +0100 Subject: [PATCH 2/8] added curved corners to connectors --- src/elements/connector.rs | 72 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/elements/connector.rs b/src/elements/connector.rs index 42e56c6..5255b8c 100644 --- a/src/elements/connector.rs +++ b/src/elements/connector.rs @@ -78,6 +78,7 @@ pub struct Connector { end: Endpoint, conn_type: ConnectionType, offset: Option, + corner_radius: f32, } fn closest_loc( @@ -241,6 +242,13 @@ impl Connector { None }; + let corner_radius = if let Some(rad) = element.pop_attr("corner-radius") { + (&rad).parse() + .map_err(|_| SvgdxError::ParseError("Invalid corner-radius".to_owned()))? + } else { + 0.0 + }; + // This could probably be tidier, trying to deal with lots of combinations. // Needs to support explicit coordinate pairs or element references, and // for element references support given locations or not (in which case @@ -336,6 +344,7 @@ impl Connector { end_el: end_el.cloned(), conn_type, offset, + corner_radius, }) } @@ -764,8 +773,20 @@ impl Connector { abs_offset_set, )?; + + // TODO: remove repeated points. - if points.len() == 2 { + if self.corner_radius != 0.0{ + SvgElement::new( + "path", + &[( + "d".to_string(), + Self::points_to_path(points,self.corner_radius) + )], + ) + .with_attrs_from(&self.source_element) + } + else if points.len() == 2 { SvgElement::new( "line", &[ @@ -794,4 +815,53 @@ impl Connector { }; Ok(conn_element) } + + fn points_to_path(points: Vec<(f32,f32)>, max_radius: f32) -> String{ + let mut result = String::new(); + let mut radii = vec![]; + for i in 1..(points.len()-1){ + let mut d1 = (points[i].0-points[i-1].0).abs() + (points[i].1-points[i-1].1).abs(); + let mut d2 = (points[i+1].0-points[i].0).abs() + (points[i+1].1-points[i].1).abs(); + if i != 1{ + d1 = d1/2.0; + } + if i != points.len()-2{ + d2 = d2/2.0; + } + let radius = d1.min(d2).min(max_radius); + radii.push(radius); + } + + let mut pos = points[0]; + result += &("M ".to_owned() + &pos.0.to_string() + "," + &pos.1.to_string() + "\n"); + + for i in 1..(points.len()-1){ + let dx1 = points[i].0-pos.0; + let dy1 = points[i].1-pos.1; + let dx2 = points[i+1].0-points[i].0; + let dy2 = points[i+1].1-points[i].1; + + pos.0 = pos.0 + dx1 - dx1*radii[i-1]/(dx1*dx1+dy1*dy1).sqrt(); + pos.1 = pos.1 + dy1 - dy1*radii[i-1]/(dx1*dx1+dy1*dy1).sqrt(); + + result += &("L ".to_owned() + &pos.0.to_string() + "," + &pos.1.to_string() + "\n"); + + let mut new_pos = points[i]; + + new_pos.0 = new_pos.0 + dx2*radii[i-1]/(dx2*dx2+dy2*dy2).sqrt(); + new_pos.1 = new_pos.1 + dy2*radii[i-1]/(dx2*dx2+dy2*dy2).sqrt(); + + let cl = (dx1*dy2 - dy1*dx2) > 0.0; + let cl_str = if cl {"1"} else{"0"}; + + result += &("a ".to_owned() + &radii[i-1].to_string() + "," + &radii[i-1].to_string() + " 0 0 " + cl_str + " " + &(new_pos.0-pos.0).to_string() + "," + &(new_pos.1-pos.1).to_string() + "\n"); + + pos = new_pos; + + } + pos = points[points.len()-1]; + result += &("L ".to_owned() + &pos.0.to_string() + "," + &pos.1.to_string() + "\n"); + + return result; + } } From 991d07a73c5f7ad89522f9d0c3c823938a619937 Mon Sep 17 00:00:00 2001 From: megamaths Date: Tue, 5 Aug 2025 14:18:00 +0100 Subject: [PATCH 3/8] able to refer to points on a line by distance along and proportion of the line (same for other types of line) --- src/elements/connector.rs | 566 ++++++++++++++++++++++++++++++++++++-- src/elements/layout.rs | 1 + src/geometry/bbox.rs | 1 + src/geometry/types.rs | 2 + 4 files changed, 546 insertions(+), 24 deletions(-) diff --git a/src/elements/connector.rs b/src/elements/connector.rs index 5255b8c..450dc07 100644 --- a/src/elements/connector.rs +++ b/src/elements/connector.rs @@ -221,6 +221,542 @@ impl Connector { return Ok((t_el, t_loc, t_point, t_dir)); } + fn get_point_along_line( + el: &SvgElement, + dist: f32, + ) -> Result<(f32,f32)>{ + let name = el.name(); + + if name == "line"{ + 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 = x1.parse()?; + let y1: f32 = y1.parse()?; + let x2: f32 = x2.parse()?; + let y2: f32 = y2.parse()?; + if x1 == x2 && y1 == y2{ + return Ok((x1,y1)); + } + let len = ((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)).sqrt(); + let rat = dist/len; + return Ok((x1 + rat*(x2-x1), y1 + rat*(y2-y1))); + } + } + if name == "polyline"{ + if let Some(points) = el.get_attr("points"){ + let points = points.split(", "); + let mut cummulative_dist = 0.0; + let mut lastx = 0.0; + let mut lasty = 0.0; + let mut first_point = true; + for p in points{ + let mut this_point = p.split_whitespace(); + if let (Some(x),Some(y)) = (this_point.next(),this_point.next()){ + let x: f32 = x.parse()?; + let y: f32 = y.parse()?; + + if !first_point{ + let len = ((lastx-x)*(lastx-x) + (lasty-y)*(lasty-y)).sqrt(); + if cummulative_dist + len > dist{ + let rat = (dist-cummulative_dist)/len; + return Ok((lastx*(1.0-rat) + rat*x, lasty*(1.0-rat) + rat*y)); + } + cummulative_dist += len; + } + else if dist < 0.0{// clamp to start + return Ok((x,y)); + } + lastx = x; + lasty = y; + } + + first_point = false; + } + return Ok((lastx, lasty)); + } + } + if name == "path"{ + if let Some(d) = el.get_attr("d"){ + + let replaced_commas = d.replace(&[','], &" "); + let items = replaced_commas.split_whitespace(); + + let mut cummulative_distance = 0.0; + let mut pos = (0.0,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 items{ + if item.starts_with(&['a','A','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{ + if ['c','C','q','Q','s','S','t','T','z','Z'].contains(&op){ + todo!("not yet impl path parsing"); + } + else if op == 'm'{ + if arg_num == 0{ + pos.0 += item.parse::()?; + } + else if arg_num == 1{ + pos.1 += item.parse::()?; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } else if op == 'M'{ + if arg_num == 0{ + pos.0 = item.parse::()?; + } + else if arg_num == 1{ + pos.1 = item.parse::()?; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } else if op == 'h' || op == 'H'{ + if arg_num == 0{ + let val = item.parse::()?; + if op == 'h'{ + pos.0 += val; + } + else{ + pos.0 = val; + } + let d = (pos.0-last_stable_pos.0).abs(); + if cummulative_distance + d > dist{ + let r = (dist-cummulative_distance)/d; + return Ok((last_stable_pos.0*(1.0-r) + pos.0*r,pos.1)); + } + + cummulative_distance += d; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } else if op == 'v' || op == 'V'{ + if arg_num == 0{ + let val = item.parse::()?; + if op == 'v'{ + pos.1 += val; + } + else{ + pos.1 = val; + } + let d = (pos.1-last_stable_pos.1).abs(); + if cummulative_distance + d > dist{ + let r = (dist-cummulative_distance)/d; + return Ok((pos.0, last_stable_pos.1*(1.0-r) + pos.1*r)); + } + + cummulative_distance += d; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } else if op == 'l' || op == 'L'{ + if arg_num == 0{ + let val = item.parse::()?; + if op == 'l'{ + pos.0 += val; + } + else{ + pos.0 = val; + } + } + else if arg_num == 1{ + let val = item.parse::()?; + if op == 'l'{ + pos.1 += val; + } + else{ + pos.1 = val; + } + let d = ((last_stable_pos.0-pos.0)*(last_stable_pos.0-pos.0) + (last_stable_pos.1-pos.1)*(last_stable_pos.1-pos.1)).sqrt(); + if cummulative_distance + d > dist{ + let r = (dist-cummulative_distance)/d; + return Ok((last_stable_pos.0*(1.0-r) + pos.0*r,last_stable_pos.1*(1.0-r) + pos.1*r)); + } + + cummulative_distance += d; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } else if op == 'a' || op == 'A'{ + if arg_num == 0{ + let val = item.parse::()?; + r = val; + } + else if arg_num == 1{ + let val = item.parse::()?; + 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 = item.parse::()?; + if op == 'a'{ + pos.0 += val; + } + else{ + pos.0 = val; + } + } + else if arg_num == 6{ + let val = item.parse::()?; + 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{ + centre = 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()}; + centre = (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; + } + 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 cummulative_distance + arc_length > dist{ + let ratio = (dist-cummulative_distance)/arc_length; + let final_angle = ang_1 + arc_angle*ratio; + + return Ok((centre.0 + r*(final_angle).cos(), centre.1 + r*(final_angle).sin())); + } + + cummulative_distance += arc_length; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } + + arg_num += 1; + } + } + return Ok(pos); + } + } + + return Err(SvgdxError::MissingAttribute("in either line, polyline or path".to_string())) + } + + fn get_line_length( + el: &SvgElement, + ) -> Result{ + let name = el.name(); + + let mut sum = 0.0; + if name == "line"{ + 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 = x1.parse()?; + let y1: f32 = y1.parse()?; + let x2: f32 = x2.parse()?; + let y2: f32 = y2.parse()?; + sum += ((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)).sqrt(); + } + } + if name == "polyline"{ + if let Some(points) = el.get_attr("points"){ + let points = points.split(", "); + let mut lastx: f32 = 0.0; + let mut lasty: f32 = 0.0; + let mut first_point = true; + for p in points{ + let mut this_point = p.split_whitespace(); + if let (Some(x),Some(y)) = (this_point.next(),this_point.next()){ + let x: f32 = x.parse()?; + let y: f32 = y.parse()?; + + if !first_point{ + let len: f32 = ((lastx-x)*(lastx-x) + (lasty-y)*(lasty-y)).sqrt(); + sum += len; + } + lastx = x; + lasty = y; + } + + first_point = false; + } + } + } + if name == "path"{ + + if let Some(d) = el.get_attr("d"){ + + let replaced_commas = d.replace(&[','], &" "); + let items = replaced_commas.split_whitespace(); + + let mut cummulative_distance = 0.0; + let mut pos = (0.0,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 items{ + if item.starts_with(&['a','A','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; + } + } + else{ + if ['c','C','q','Q','s','S','t','T','z','Z'].contains(&op){ + todo!("not yet impl path parsing"); + } + else if op == 'm'{ + if arg_num == 0{ + pos.0 += item.parse::()?; + } + else if arg_num == 1{ + pos.1 += item.parse::()?; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } else if op == 'M'{ + if arg_num == 0{ + pos.0 = item.parse::()?; + } + else if arg_num == 1{ + pos.1 = item.parse::()?; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } else if op == 'h' || op == 'H'{ + if arg_num == 0{ + let val = item.parse::()?; + if op == 'h'{ + pos.0 += val; + } + else{ + pos.0 = val; + } + let d = (pos.0-last_stable_pos.0).abs(); + + + cummulative_distance += d; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } else if op == 'v' || op == 'V'{ + if arg_num == 0{ + let val = item.parse::()?; + if op == 'v'{ + pos.1 += val; + } + else{ + pos.1 = val; + } + let d = (pos.1-last_stable_pos.1).abs(); + + + cummulative_distance += d; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } else if op == 'l' || op == 'L'{ + if arg_num == 0{ + let val = item.parse::()?; + if op == 'l'{ + pos.0 += val; + } + else{ + pos.0 = val; + } + } + else if arg_num == 1{ + let val = item.parse::()?; + if op == 'l'{ + pos.1 += val; + } + else{ + pos.1 = val; + } + let d = ((last_stable_pos.0-pos.0)*(last_stable_pos.0-pos.0) + (last_stable_pos.1-pos.1)*(last_stable_pos.1-pos.1)).sqrt(); + + + cummulative_distance += d; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } else if op == 'a' || op == 'A'{ + if arg_num == 0{ + let val = item.parse::()?; + r = val; + } + else if arg_num == 1{ + let val = item.parse::()?; + 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 = item.parse::()?; + if op == 'a'{ + pos.0 += val; + } + else{ + pos.0 = val; + } + } + else if arg_num == 6{ + let val = item.parse::()?; + 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{ + centre = 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()}; + centre = (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; + } + 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; + + + + cummulative_distance += arc_length; + last_stable_pos = pos; + } + else{ + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } + } + + arg_num += 1; + } + } + sum += cummulative_distance; + } + } + + return Ok(sum); + } + + fn get_coord_element_loc( + elem_map: &impl ElementMap, + el: &SvgElement, + loc: LocSpec, + ) -> Result<(f32,f32)>{ + + if let LocSpec::PureLength(l) = loc { + if let Length::Ratio(r) = l{ + let ll = Self::get_line_length(el)?; + return Self::get_point_along_line(el, ll*r); + } + if let Length::Absolute(a) = l{ + return Self::get_point_along_line(el, a); + } + } + + let coord = elem_map + .get_element_bbox(el)? + .ok_or_else(|| SvgdxError::MissingBoundingBox(el.to_string()))? + .locspec(loc); + + return Ok(coord); + } + pub fn from_element( element: &SvgElement, elem_map: &impl ElementMap, @@ -267,10 +803,7 @@ impl Connector { end_loc = Some(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 = Self::get_coord_element_loc(elem_map, end_el, end_loc.expect("Set from closest_loc"))?; ( Endpoint::new(start_point, start_dir), Endpoint::new(end_coord, end_dir), @@ -284,10 +817,7 @@ impl Connector { start_loc = Some(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 = Self::get_coord_element_loc(elem_map, start_el, start_loc.expect("Set from closest_loc"))?; ( Endpoint::new(start_coord, start_dir), Endpoint::new(end_point, end_dir), @@ -306,30 +836,18 @@ impl Connector { 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 = Self::get_coord_element_loc(elem_map, end_el, end_loc.expect("Not both None"))?; let sloc = closest_loc(start_el, end_coord, conn_type, elem_map)?; start_loc = Some(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 = Self::get_coord_element_loc(elem_map, start_el, start_loc.expect("Not both None"))?; let eloc = closest_loc(end_el, start_coord, conn_type, elem_map)?; end_loc = Some(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 = Self::get_coord_element_loc(elem_map, start_el, start_loc.expect("Set above"))?; + let end_coord = Self::get_coord_element_loc(elem_map, end_el, end_loc.expect("Set above"))?; ( Endpoint::new(start_coord, start_dir), Endpoint::new(end_coord, end_dir), diff --git a/src/elements/layout.rs b/src/elements/layout.rs index 8a89cc1..afc4456 100644 --- a/src/elements/layout.rs +++ b/src/elements/layout.rs @@ -893,6 +893,7 @@ fn eval_text_anchor(element: &mut SvgElement, ctx: &impl ContextView) -> Result< LocSpec::BottomEdge(_) => element.set_default_attr("text-loc", "b"), LocSpec::LeftEdge(_) => element.set_default_attr("text-loc", "l"), LocSpec::RightEdge(_) => element.set_default_attr("text-loc", "r"), + LocSpec::PureLength(_) => element.set_default_attr("text-loc", "c"),// not sure } } else { return Err(SvgdxError::InvalidData(format!( diff --git a/src/geometry/bbox.rs b/src/geometry/bbox.rs index 7e120d9..e4d6d3a 100644 --- a/src/geometry/bbox.rs +++ b/src/geometry/bbox.rs @@ -65,6 +65,7 @@ impl BoundingBox { RightEdge(len) => (self.x2, len.calc_offset(self.y1, self.y2)), BottomEdge(len) => (len.calc_offset(self.x1, self.x2), self.y2), LeftEdge(len) => (self.x1, len.calc_offset(self.y1, self.y2)), + PureLength(len) => panic!(), } } diff --git a/src/geometry/types.rs b/src/geometry/types.rs index e724739..f6716cf 100644 --- a/src/geometry/types.rs +++ b/src/geometry/types.rs @@ -139,6 +139,7 @@ pub enum LocSpec { RightEdge(Length), BottomEdge(Length), LeftEdge(Length), + PureLength(Length), } impl LocSpec { @@ -193,6 +194,7 @@ impl FromStr for LocSpec { "r" => Ok(Self::RightEdge(len)), "b" => Ok(Self::BottomEdge(len)), "l" => Ok(Self::LeftEdge(len)), + "" => Ok(Self::PureLength(len)), _ => Err(SvgdxError::InvalidData(format!( "Invalid LocSpec format {value}" ))), From 71d30bcb406aef692c3d7fb91742ab0ef7c7240a Mon Sep 17 00:00:00 2001 From: megamaths Date: Wed, 6 Aug 2025 12:47:11 +0100 Subject: [PATCH 4/8] splitting into multiple functions and clippy compliance --- src/elements/connector.rs | 1408 ++++++++++++++++++------------------- src/elements/layout.rs | 2 +- src/geometry/bbox.rs | 2 +- 3 files changed, 705 insertions(+), 707 deletions(-) diff --git a/src/elements/connector.rs b/src/elements/connector.rs index 450dc07..474564a 100644 --- a/src/elements/connector.rs +++ b/src/elements/connector.rs @@ -163,6 +163,12 @@ impl PartialOrd for HeapData { Some(self.cmp(other)) } } +struct ElementParseData<'a> { + el: Option<&'a SvgElement>, + loc: Option, + point: Option<(f32, f32)>, + dir: Option, +} impl Connector { fn loc_to_dir(loc: LocSpec) -> Option { @@ -179,32 +185,29 @@ impl Connector { element: &mut SvgElement, elem_map: &'a impl ElementMap, start: bool, - ) -> Result<( - Option<&'a SvgElement>, - Option, - Option<(f32, f32)>, - Option, - )> { + ) -> Result> { let attrib_name = if start { "start" } else { "end" }; let this_ref = element .pop_attr(attrib_name) .ok_or_else(|| SvgdxError::MissingAttribute(attrib_name.to_string()))?; - let mut t_el: Option<&SvgElement> = None; - let mut t_loc: Option = None; - let mut t_point: Option<(f32, f32)> = None; - let mut t_dir: Option = None; + let mut ret = ElementParseData { + el: None, + loc: None, + point: None, + dir: None, + }; // Example: "#thing@tl" => top left coordinate of element id="thing" if let Ok((elref, loc)) = parse_el_loc(&this_ref) { if let Some(loc) = loc { - t_dir = Self::loc_to_dir(loc); - t_loc = Some(loc); + ret.dir = Self::loc_to_dir(loc); + ret.loc = Some(loc); } - t_el = elem_map.get_element(&elref); + ret.el = elem_map.get_element(&elref); } else { let mut parts = attr_split(&this_ref).map_while(|v| strp(&v).ok()); - t_point = Some(( + ret.point = Some(( parts.next().ok_or_else(|| { SvgdxError::InvalidData( (attrib_name.to_owned() + "_ref x should be numeric").to_owned(), @@ -218,52 +221,96 @@ impl Connector { )); } - return Ok((t_el, t_loc, t_point, t_dir)); + Ok(ret) } - fn get_point_along_line( - el: &SvgElement, - dist: f32, - ) -> Result<(f32,f32)>{ - let name = el.name(); + fn get_point_along_line(el: &SvgElement, length: Length) -> Result<(f32, f32)> { + let is_percent; + let dist; + match length { + Length::Absolute(abs) => { + dist = abs; + is_percent = false; + } + Length::Ratio(rat) => { + dist = rat; + is_percent = true; + } + } - if name == "line"{ - 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 = x1.parse()?; - let y1: f32 = y1.parse()?; - let x2: f32 = x2.parse()?; - let y2: f32 = y2.parse()?; - if x1 == x2 && y1 == y2{ - return Ok((x1,y1)); - } - let len = ((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)).sqrt(); - let rat = dist/len; - return Ok((x1 + rat*(x2-x1), y1 + rat*(y2-y1))); + 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 = x1.parse()?; + let y1: f32 = y1.parse()?; + let x2: f32 = x2.parse()?; + let y2: f32 = y2.parse()?; + if x1 == x2 && y1 == y2 { + return Ok((x1, y1)); } + + let rat = if is_percent { + dist + } else { + let len = ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)).sqrt(); + dist / len + }; + return Ok((x1 + rat * (x2 - x1), y1 + rat * (y2 - y1))); } - if name == "polyline"{ - if let Some(points) = el.get_attr("points"){ - let points = points.split(", "); + + 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 mut is_percent; + let mut dist; + match length { + Length::Absolute(abs) => { + dist = abs; + is_percent = false; + } + Length::Ratio(rat) => { + dist = rat; + is_percent = true; + } + } + + if let Some(points) = el.get_attr("points") { + let points = points.split(", "); + let mut lastx; + let mut lasty; + + // loop to allow repeat to find total length if a percentage + loop { let mut cummulative_dist = 0.0; - let mut lastx = 0.0; - let mut lasty = 0.0; + lastx = 0.0; + lasty = 0.0; let mut first_point = true; - for p in points{ + for p in points.clone() { let mut this_point = p.split_whitespace(); - if let (Some(x),Some(y)) = (this_point.next(),this_point.next()){ + if let (Some(x), Some(y)) = (this_point.next(), this_point.next()) { let x: f32 = x.parse()?; let y: f32 = y.parse()?; - if !first_point{ - let len = ((lastx-x)*(lastx-x) + (lasty-y)*(lasty-y)).sqrt(); - if cummulative_dist + len > dist{ - let rat = (dist-cummulative_dist)/len; - return Ok((lastx*(1.0-rat) + rat*x, lasty*(1.0-rat) + rat*y)); + if !first_point { + let len = + ((lastx - x) * (lastx - x) + (lasty - y) * (lasty - y)).sqrt(); + if !is_percent && cummulative_dist + len > dist { + let rat = (dist - cummulative_dist) / len; + return Ok(( + lastx * (1.0 - rat) + rat * x, + lasty * (1.0 - rat) + rat * y, + )); } cummulative_dist += len; - } - else if dist < 0.0{// clamp to start - return Ok((x,y)); + } else if dist < 0.0 { + // clamp to start + return Ok((x, y)); } lastx = x; lasty = y; @@ -271,17 +318,44 @@ impl Connector { first_point = false; } - return Ok((lastx, lasty)); + if !is_percent { + break; + } else { + is_percent = false; + dist *= cummulative_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 mut is_percent; + let mut dist; + match length { + Length::Absolute(abs) => { + dist = abs; + is_percent = false; + } + Length::Ratio(rat) => { + dist = rat; + is_percent = true; } } - if name == "path"{ - if let Some(d) = el.get_attr("d"){ - let replaced_commas = d.replace(&[','], &" "); - let items = replaced_commas.split_whitespace(); + if let Some(d) = el.get_attr("d") { + let replaced_commas = d.replace([','], " "); + let items = replaced_commas.split_whitespace(); - let mut cummulative_distance = 0.0; - let mut pos = (0.0,0.0); + let mut pos; + // loop to allow repeat to find total length if a percentage + loop { + let mut cummulative_dist = 0.0; + pos = (0.0, 0.0); let mut last_stable_pos = pos; let mut r = 0.0; let mut large_arc_flag = false; @@ -289,464 +363,254 @@ impl Connector { let mut op = ' '; let mut arg_num = 0; - for item in items{ - if item.starts_with(&['a','A','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(){ + for item in items.clone() { + if item.starts_with([ + 'a', 'A', '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 + if dist < 0.0 && !['m', 'M'].contains(&c) { + // clamping the start return Ok(last_stable_pos); } } - } - else{ - if ['c','C','q','Q','s','S','t','T','z','Z'].contains(&op){ + } else { + if ['c', 'C', 'q', 'Q', 's', 'S', 't', 'T', 'z', 'Z'].contains(&op) { todo!("not yet impl path parsing"); - } - else if op == 'm'{ - if arg_num == 0{ + } else if op == 'm' { + if arg_num == 0 { pos.0 += item.parse::()?; - } - else if arg_num == 1{ + } else if arg_num == 1 { pos.1 += item.parse::()?; last_stable_pos = pos; + } else { + return Err(SvgdxError::ParseError( + "path has too many vars".to_string(), + )); } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); - } - } else if op == 'M'{ - if arg_num == 0{ + } else if op == 'M' { + if arg_num == 0 { pos.0 = item.parse::()?; - } - else if arg_num == 1{ + } else if arg_num == 1 { pos.1 = item.parse::()?; last_stable_pos = pos; + } else { + return Err(SvgdxError::ParseError( + "path has too many vars".to_string(), + )); } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); - } - } else if op == 'h' || op == 'H'{ - if arg_num == 0{ + } else if op == 'h' || op == 'H' { + if arg_num == 0 { let val = item.parse::()?; - if op == 'h'{ + if op == 'h' { pos.0 += val; - } - else{ + } else { pos.0 = val; } - let d = (pos.0-last_stable_pos.0).abs(); - if cummulative_distance + d > dist{ - let r = (dist-cummulative_distance)/d; - return Ok((last_stable_pos.0*(1.0-r) + pos.0*r,pos.1)); + let d = (pos.0 - last_stable_pos.0).abs(); + if !is_percent && cummulative_dist + d > dist { + let r = (dist - cummulative_dist) / d; + return Ok((last_stable_pos.0 * (1.0 - r) + pos.0 * r, pos.1)); } - cummulative_distance += d; + cummulative_dist += d; last_stable_pos = pos; + } else { + return Err(SvgdxError::ParseError( + "path has too many vars".to_string(), + )); } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); - } - } else if op == 'v' || op == 'V'{ - if arg_num == 0{ + } else if op == 'v' || op == 'V' { + if arg_num == 0 { let val = item.parse::()?; - if op == 'v'{ + if op == 'v' { pos.1 += val; - } - else{ + } else { pos.1 = val; } - let d = (pos.1-last_stable_pos.1).abs(); - if cummulative_distance + d > dist{ - let r = (dist-cummulative_distance)/d; - return Ok((pos.0, last_stable_pos.1*(1.0-r) + pos.1*r)); + let d = (pos.1 - last_stable_pos.1).abs(); + if !is_percent && cummulative_dist + d > dist { + let r = (dist - cummulative_dist) / d; + return Ok((pos.0, last_stable_pos.1 * (1.0 - r) + pos.1 * r)); } - cummulative_distance += d; + cummulative_dist += d; last_stable_pos = pos; + } else { + return Err(SvgdxError::ParseError( + "path has too many vars".to_string(), + )); } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); - } - } else if op == 'l' || op == 'L'{ - if arg_num == 0{ + } else if op == 'l' || op == 'L' { + if arg_num == 0 { let val = item.parse::()?; - if op == 'l'{ + if op == 'l' { pos.0 += val; - } - else{ + } else { pos.0 = val; } - } - else if arg_num == 1{ + } else if arg_num == 1 { let val = item.parse::()?; - if op == 'l'{ + if op == 'l' { pos.1 += val; - } - else{ + } else { pos.1 = val; } - let d = ((last_stable_pos.0-pos.0)*(last_stable_pos.0-pos.0) + (last_stable_pos.1-pos.1)*(last_stable_pos.1-pos.1)).sqrt(); - if cummulative_distance + d > dist{ - let r = (dist-cummulative_distance)/d; - return Ok((last_stable_pos.0*(1.0-r) + pos.0*r,last_stable_pos.1*(1.0-r) + pos.1*r)); + let d = ((last_stable_pos.0 - pos.0) * (last_stable_pos.0 - pos.0) + + (last_stable_pos.1 - pos.1) * (last_stable_pos.1 - pos.1)) + .sqrt(); + if !is_percent && cummulative_dist + d > dist { + let r = (dist - cummulative_dist) / d; + return Ok(( + last_stable_pos.0 * (1.0 - r) + pos.0 * r, + last_stable_pos.1 * (1.0 - r) + pos.1 * r, + )); } - cummulative_distance += d; + cummulative_dist += d; last_stable_pos = pos; + } else { + return Err(SvgdxError::ParseError( + "path has too many vars".to_string(), + )); } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); - } - } else if op == 'a' || op == 'A'{ - if arg_num == 0{ + } else if op == 'a' || op == 'A' { + if arg_num == 0 { let val = item.parse::()?; r = val; - } - else if arg_num == 1{ + } else if arg_num == 1 { let val = item.parse::()?; - if r != val{ - return Err(SvgdxError::ParseError("path length not supported for non circle elipse".to_string())); + if r != val { + return Err(SvgdxError::ParseError( + "path length not supported for non circle elipse" + .to_string(), + )); } - } - else if arg_num == 2{ + } else if arg_num == 2 { // unused as not mean anything for circle - } - else if arg_num == 3{ + } else if arg_num == 3 { let val = item.parse::()?; large_arc_flag = val != 0; - } - else if arg_num == 4{ + } else if arg_num == 4 { let val = item.parse::()?; sweeping_flag = val != 0; - } - else if arg_num == 5{ + } else if arg_num == 5 { let val = item.parse::()?; - if op == 'a'{ + if op == 'a' { pos.0 += val; - } - else{ + } else { pos.0 = val; } - } - else if arg_num == 6{ + } else if arg_num == 6 { let val = item.parse::()?; - if op == 'a'{ + if op == 'a' { pos.1 += val; - } - else{ + } 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 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{ - centre = 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()}; - centre = (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; + + 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 { + 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; } - 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 cummulative_distance + arc_length > dist{ - let ratio = (dist-cummulative_distance)/arc_length; - let final_angle = ang_1 + arc_angle*ratio; - - return Ok((centre.0 + r*(final_angle).cos(), centre.1 + r*(final_angle).sin())); + 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 && cummulative_dist + arc_length > dist { + let ratio = (dist - cummulative_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(), + )); } - cummulative_distance += arc_length; + cummulative_dist += arc_length; last_stable_pos = pos; - } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } else { + return Err(SvgdxError::ParseError( + "path has too many vars".to_string(), + )); } } arg_num += 1; } } - return Ok(pos); + if !is_percent { + break; + } else { + is_percent = false; + dist *= cummulative_dist; + } } + return Ok(pos); } - return Err(SvgdxError::MissingAttribute("in either line, polyline or path".to_string())) + Err(SvgdxError::MissingAttribute("d in path".to_string())) } - fn get_line_length( - el: &SvgElement, - ) -> Result{ + fn get_point_along_linish_type_el(el: &SvgElement, length: Length) -> Result<(f32, f32)> { let name = el.name(); - let mut sum = 0.0; - if name == "line"{ - 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 = x1.parse()?; - let y1: f32 = y1.parse()?; - let x2: f32 = x2.parse()?; - let y2: f32 = y2.parse()?; - sum += ((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)).sqrt(); - } + if name == "line" { + return Self::get_point_along_line(el, length); } - if name == "polyline"{ - if let Some(points) = el.get_attr("points"){ - let points = points.split(", "); - let mut lastx: f32 = 0.0; - let mut lasty: f32 = 0.0; - let mut first_point = true; - for p in points{ - let mut this_point = p.split_whitespace(); - if let (Some(x),Some(y)) = (this_point.next(),this_point.next()){ - let x: f32 = x.parse()?; - let y: f32 = y.parse()?; - - if !first_point{ - let len: f32 = ((lastx-x)*(lastx-x) + (lasty-y)*(lasty-y)).sqrt(); - sum += len; - } - lastx = x; - lasty = y; - } - - first_point = false; - } - } + if name == "polyline" { + return Self::get_point_along_polyline(el, length); } - if name == "path"{ - - if let Some(d) = el.get_attr("d"){ - - let replaced_commas = d.replace(&[','], &" "); - let items = replaced_commas.split_whitespace(); - - let mut cummulative_distance = 0.0; - let mut pos = (0.0,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 items{ - if item.starts_with(&['a','A','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; - } - } - else{ - if ['c','C','q','Q','s','S','t','T','z','Z'].contains(&op){ - todo!("not yet impl path parsing"); - } - else if op == 'm'{ - if arg_num == 0{ - pos.0 += item.parse::()?; - } - else if arg_num == 1{ - pos.1 += item.parse::()?; - last_stable_pos = pos; - } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); - } - } else if op == 'M'{ - if arg_num == 0{ - pos.0 = item.parse::()?; - } - else if arg_num == 1{ - pos.1 = item.parse::()?; - last_stable_pos = pos; - } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); - } - } else if op == 'h' || op == 'H'{ - if arg_num == 0{ - let val = item.parse::()?; - if op == 'h'{ - pos.0 += val; - } - else{ - pos.0 = val; - } - let d = (pos.0-last_stable_pos.0).abs(); - - - cummulative_distance += d; - last_stable_pos = pos; - } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); - } - } else if op == 'v' || op == 'V'{ - if arg_num == 0{ - let val = item.parse::()?; - if op == 'v'{ - pos.1 += val; - } - else{ - pos.1 = val; - } - let d = (pos.1-last_stable_pos.1).abs(); - - - cummulative_distance += d; - last_stable_pos = pos; - } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); - } - } else if op == 'l' || op == 'L'{ - if arg_num == 0{ - let val = item.parse::()?; - if op == 'l'{ - pos.0 += val; - } - else{ - pos.0 = val; - } - } - else if arg_num == 1{ - let val = item.parse::()?; - if op == 'l'{ - pos.1 += val; - } - else{ - pos.1 = val; - } - let d = ((last_stable_pos.0-pos.0)*(last_stable_pos.0-pos.0) + (last_stable_pos.1-pos.1)*(last_stable_pos.1-pos.1)).sqrt(); - - - cummulative_distance += d; - last_stable_pos = pos; - } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); - } - } else if op == 'a' || op == 'A'{ - if arg_num == 0{ - let val = item.parse::()?; - r = val; - } - else if arg_num == 1{ - let val = item.parse::()?; - 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 = item.parse::()?; - if op == 'a'{ - pos.0 += val; - } - else{ - pos.0 = val; - } - } - else if arg_num == 6{ - let val = item.parse::()?; - 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{ - centre = 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()}; - centre = (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; - } - 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; - - - - cummulative_distance += arc_length; - last_stable_pos = pos; - } - else{ - return Err(SvgdxError::ParseError("path has too many vars".to_string())); - } - } - - arg_num += 1; - } - } - sum += cummulative_distance; - } + if name == "path" { + return Self::get_point_along_path(el, length); } - return Ok(sum); + Err(SvgdxError::MissingAttribute( + "looking for point on line in a non line element".to_string(), + )) } fn get_coord_element_loc( elem_map: &impl ElementMap, el: &SvgElement, loc: LocSpec, - ) -> Result<(f32,f32)>{ - + ) -> Result<(f32, f32)> { if let LocSpec::PureLength(l) = loc { - if let Length::Ratio(r) = l{ - let ll = Self::get_line_length(el)?; - return Self::get_point_along_line(el, ll*r); - } - if let Length::Absolute(a) = l{ - return Self::get_point_along_line(el, a); - } + return Self::get_point_along_linish_type_el(el, l); } let coord = elem_map @@ -754,7 +618,7 @@ impl Connector { .ok_or_else(|| SvgdxError::MissingBoundingBox(el.to_string()))? .locspec(loc); - return Ok(coord); + Ok(coord) } pub fn from_element( @@ -764,10 +628,12 @@ impl Connector { ) -> Result { let mut element = element.clone(); + let start_ret = Self::parse_element(&mut element, elem_map, true)?; + let end_ret = Self::parse_element(&mut element, elem_map, false)?; let (start_el, mut start_loc, start_point, mut start_dir) = - Self::parse_element(&mut element, elem_map, true)?; + (start_ret.el, start_ret.loc, start_ret.point, start_ret.dir); let (end_el, mut end_loc, end_point, mut end_dir) = - Self::parse_element(&mut element, elem_map, false)?; + (end_ret.el, end_ret.loc, end_ret.point, end_ret.dir); let offset = if let Some(o_inner) = element.pop_attr("corner-offset") { Some( @@ -779,7 +645,7 @@ impl Connector { }; let corner_radius = if let Some(rad) = element.pop_attr("corner-radius") { - (&rad).parse() + rad.parse() .map_err(|_| SvgdxError::ParseError("Invalid corner-radius".to_owned()))? } else { 0.0 @@ -803,7 +669,11 @@ impl Connector { end_loc = Some(eloc); end_dir = Self::loc_to_dir(eloc); } - let end_coord = Self::get_coord_element_loc(elem_map, end_el, end_loc.expect("Set from closest_loc"))?; + let end_coord = Self::get_coord_element_loc( + elem_map, + end_el, + end_loc.expect("Set from closest_loc"), + )?; ( Endpoint::new(start_point, start_dir), Endpoint::new(end_coord, end_dir), @@ -817,7 +687,11 @@ impl Connector { start_loc = Some(sloc); start_dir = Self::loc_to_dir(sloc); } - let start_coord = Self::get_coord_element_loc(elem_map, start_el, start_loc.expect("Set from closest_loc"))?; + let start_coord = Self::get_coord_element_loc( + elem_map, + start_el, + start_loc.expect("Set from closest_loc"), + )?; ( Endpoint::new(start_coord, start_dir), Endpoint::new(end_point, end_dir), @@ -836,18 +710,28 @@ impl Connector { start_dir = Self::loc_to_dir(sloc); end_dir = Self::loc_to_dir(eloc); } else if start_loc.is_none() { - let end_coord = Self::get_coord_element_loc(elem_map, end_el, end_loc.expect("Not both None"))?; + let end_coord = Self::get_coord_element_loc( + elem_map, + end_el, + end_loc.expect("Not both None"), + )?; let sloc = closest_loc(start_el, end_coord, conn_type, elem_map)?; start_loc = Some(sloc); start_dir = Self::loc_to_dir(sloc); } else if end_loc.is_none() { - let start_coord = Self::get_coord_element_loc(elem_map, start_el, start_loc.expect("Not both None"))?; + let start_coord = Self::get_coord_element_loc( + elem_map, + start_el, + start_loc.expect("Not both None"), + )?; let eloc = closest_loc(end_el, start_coord, conn_type, elem_map)?; end_loc = Some(eloc); end_dir = Self::loc_to_dir(eloc); } - let start_coord = Self::get_coord_element_loc(elem_map, start_el, start_loc.expect("Set above"))?; - let end_coord = Self::get_coord_element_loc(elem_map, end_el, end_loc.expect("Set above"))?; + let start_coord = + Self::get_coord_element_loc(elem_map, start_el, start_loc.expect("Set above"))?; + let end_coord = + Self::get_coord_element_loc(elem_map, end_el, end_loc.expect("Set above"))?; ( Endpoint::new(start_coord, start_dir), Endpoint::new(end_coord, end_dir), @@ -883,291 +767,397 @@ impl Connector { } } - return true; + true } - fn render_match_conner( + fn render_match_corner_get_lines( &self, ratio_offset: f32, - start_abs_offset: f32, - end_abs_offset: f32, - sel_bb: BoundingBox, - eel_bb: BoundingBox, + abs_offsets: (f32, f32), + bbs: (BoundingBox, BoundingBox), abs_offset_set: bool, - ) -> Result> { + dirs: (Direction, Direction), + ) -> (Vec, Vec, usize, usize) { let (x1, y1) = self.start.origin; let (x2, y2) = self.end.origin; + let sel_bb = bbs.0; + let eel_bb = bbs.1; + let start_abs_offset = abs_offsets.0; + let end_abs_offset = abs_offsets.1; + let start_dir = dirs.0; + let end_dir = dirs.1; + + 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(sel_bb.x1 - start_abs_offset); + x_lines.push(sel_bb.x2 + start_abs_offset); + x_lines.push(eel_bb.x1 - end_abs_offset); + x_lines.push(eel_bb.x2 + end_abs_offset); + + if sel_bb.x1 > eel_bb.x2 { + // there is a gap + x_lines.push((sel_bb.x1 + eel_bb.x2) * 0.5); + mid_x = x_lines.len() - 1; + } else if sel_bb.x2 < eel_bb.x1 { + // there is a gap + x_lines.push((sel_bb.x2 + eel_bb.x1) * 0.5); + mid_x = x_lines.len() - 1; + } - // method generates all points it could possibly want to go through then does dijkstras on it + y_lines.push(sel_bb.y1 - start_abs_offset); + y_lines.push(sel_bb.y2 + start_abs_offset); + y_lines.push(eel_bb.y1 - end_abs_offset); + y_lines.push(eel_bb.y2 + end_abs_offset); + + if sel_bb.y1 > eel_bb.y2 { + // there is a gap + y_lines.push(sel_bb.y1 * (1.0 - ratio_offset) + eel_bb.y2 * ratio_offset); + mid_y = y_lines.len() - 1; + } else if sel_bb.y2 < eel_bb.y1 { + // there is a gap + y_lines.push(sel_bb.y2 * (1.0 - ratio_offset) + eel_bb.y1 * ratio_offset); + mid_y = y_lines.len() - 1; + } - let mut points: Vec<(f32, f32)>; - if let (Some(start_dir_some), Some(end_dir_some)) = (self.start.dir, self.end.dir) { - points = vec![]; + match start_dir { + Direction::Left | Direction::Right => { + y_lines.push(y1); + } + Direction::Down | Direction::Up => { + x_lines.push(x1); + } + } - // x_lines have constant x vary over y - let mut x_lines = vec![]; - let mut y_lines = vec![]; - let mut point_set = vec![]; - let mut mid_x = std::usize::MAX; - let mut mid_y = std::usize::MAX; - - x_lines.push(sel_bb.x1 - start_abs_offset); - x_lines.push(sel_bb.x2 + start_abs_offset); - x_lines.push(eel_bb.x1 - end_abs_offset); - x_lines.push(eel_bb.x2 + end_abs_offset); - - if sel_bb.x1 > eel_bb.x2 { - // there is a gap - x_lines.push((sel_bb.x1 + eel_bb.x2) * 0.5); - mid_x = x_lines.len() - 1; - } else if sel_bb.x2 < eel_bb.x1 { - // there is a gap - x_lines.push((sel_bb.x2 + eel_bb.x1) * 0.5); - mid_x = x_lines.len() - 1; + match end_dir { + Direction::Left | Direction::Right => { + y_lines.push(y2); + } + Direction::Down | Direction::Up => { + x_lines.push(x2); } + } - y_lines.push(sel_bb.y1 - start_abs_offset); - y_lines.push(sel_bb.y2 + start_abs_offset); - y_lines.push(eel_bb.y1 - end_abs_offset); - y_lines.push(eel_bb.y2 + end_abs_offset); - - if sel_bb.y1 > eel_bb.y2 { - // there is a gap - y_lines.push(sel_bb.y1 * (1.0 - ratio_offset) + eel_bb.y2 * ratio_offset); - mid_y = y_lines.len() - 1; - } else if sel_bb.y2 < eel_bb.y1 { - // there is a gap - y_lines.push(sel_bb.y2 * (1.0 - ratio_offset) + eel_bb.y1 * ratio_offset); - mid_y = y_lines.len() - 1; + if abs_offset_set { + match start_dir { + Direction::Down => mid_y = 1, // positive x + Direction::Left => mid_x = 0, // negative y + Direction::Right => mid_x = 1, // positive y + Direction::Up => mid_y = 0, // positive x } + } + + (x_lines, y_lines, mid_x, mid_y) + } - match start_dir_some { - Direction::Left | Direction::Right => { - y_lines.push(y1); + fn render_match_corner_get_edges( + point_set: &[(f32, f32)], + sel_bb: BoundingBox, + eel_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; } - Direction::Down | Direction::Up => { - x_lines.push(x1); + let mut connected = false; + + // check if not blocked by a wall + if point_set[i].0 == point_set[j].0 + && !Self::aals_blocked_by_bb( + sel_bb, + point_set[i].1, + point_set[j].1, + false, + point_set[i].0, + ) + && !Self::aals_blocked_by_bb( + eel_bb, + point_set[i].1, + point_set[j].1, + false, + point_set[i].0, + ) + { + connected = true; } - } - - match end_dir_some { - Direction::Left | Direction::Right => { - y_lines.push(y2); + if point_set[i].1 == point_set[j].1 + && !Self::aals_blocked_by_bb( + sel_bb, + point_set[i].0, + point_set[j].0, + true, + point_set[i].1, + ) + && !Self::aals_blocked_by_bb( + eel_bb, + point_set[i].0, + point_set[j].0, + true, + point_set[i].1, + ) + { + connected = true; } - Direction::Down | Direction::Up => { - x_lines.push(x2); + if connected { + edge_set[i].push(j); + edge_set[j].push(i); } } + } - if abs_offset_set { - match start_dir_some { - Direction::Down => mid_y = 1, // positive x - Direction::Left => mid_x = 0, // negative y - Direction::Right => mid_x = 1, // positive y - Direction::Up => mid_y = 0, // positive x - } - } + edge_set + } - for i in 0..x_lines.len() { - for j in 0..y_lines.len() { - point_set.push((x_lines[i], y_lines[j])); - } - } + fn render_match_corner_add_start_and_end( + &self, + point_set: &mut Vec<(f32, f32)>, + edge_set: &mut Vec>, + sel_bb: BoundingBox, + eel_bb: BoundingBox, + start_dir: Direction, + end_dir: Direction, + ) -> (usize, usize) { + let (x1, y1) = self.start.origin; + let (x2, y2) = self.end.origin; - let mut edge_set = vec![vec![]; point_set.len()]; + 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)) + && !Self::aals_blocked_by_bb(eel_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)) + && !Self::aals_blocked_by_bb(eel_bb, point_set[i].0, x1, true, y1) + { + edge_set[i].push(start_ind); + edge_set[start_ind].push(i); + } - 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 { - if !Self::aals_blocked_by_bb( - sel_bb, - point_set[i].1, - point_set[j].1, - false, - point_set[i].0, - ) && !Self::aals_blocked_by_bb( - eel_bb, - point_set[i].1, - point_set[j].1, - false, - point_set[i].0, - ) { - connected = true; - } - } else if point_set[i].1 == point_set[j].1 { - if !Self::aals_blocked_by_bb( - sel_bb, - point_set[i].0, - point_set[j].0, - true, - point_set[i].1, - ) && !Self::aals_blocked_by_bb( - eel_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); - } - } + if point_set[i].0 == x2 + && ((point_set[i].1 < y2 && end_dir == Direction::Up) + || (point_set[i].1 > y2 && end_dir == Direction::Down)) + && !Self::aals_blocked_by_bb(sel_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)) + && !Self::aals_blocked_by_bb(sel_bb, point_set[i].0, x2, true, y2) + { + edge_set[i].push(end_ind); + edge_set[end_ind].push(i); + } + } - // just needs to be bigger than 5* (corner cost + total bounding box size) - let inf = 1000000; - - point_set.push((x1, y1)); // start - point_set.push((x2, y2)); // end - edge_set.push(vec![]); - edge_set.push(vec![]); - let mut queue: BinaryHeap = BinaryHeap::new(); - let mut dist = vec![inf; point_set.len()]; - - 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_some == Direction::Up) - || (point_set[i].0 == x1 - && point_set[i].1 > y1 - && start_dir_some == Direction::Down) - { - if !Self::aals_blocked_by_bb(eel_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_some == Direction::Right) - || (point_set[i].1 == y1 - && point_set[i].0 < x1 - && start_dir_some == Direction::Left) - { - if !Self::aals_blocked_by_bb(eel_bb, point_set[i].0, x1, true, y1) { - edge_set[i].push(start_ind); - edge_set[start_ind].push(i); - } - } + (start_ind, end_ind) + } - if (point_set[i].0 == x2 && point_set[i].1 < y2 && end_dir_some == Direction::Up) - || (point_set[i].0 == x2 - && point_set[i].1 > y2 - && end_dir_some == Direction::Down) + fn render_match_corner_cost_function( + point_set: &[(f32, f32)], + edge_set: &[Vec], + x_lines: Vec, + y_lines: Vec, + mid_x: usize, + mid_y: usize, + ) -> Vec> { + // edge cost function + + let corner_cost = 1000; + 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] { - if !Self::aals_blocked_by_bb(sel_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_some == Direction::Right) - || (point_set[i].1 == y2 - && point_set[i].0 < x2 - && end_dir_some == Direction::Left) + 0.5 + } else { + 1.0 + }; + let mid_point_mul_y = if mid_y != usize::MAX && point_set[ind_1].1 == y_lines[mid_y] { - if !Self::aals_blocked_by_bb(sel_bb, point_set[i].0, x2, true, y2) { - edge_set[i].push(end_ind); - edge_set[end_ind].push(i); - } - } + 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 cost function - let corner_cost = 1000; - 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 != std::usize::MAX && point_set[ind_1].0 == x_lines[mid_x] { - 0.5 - } else { - 1.0 - }; - let mid_point_mul_y = - if mid_y != std::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 render_match_corner_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 = 1000000; + let mut dist = vec![inf; point_set.len()]; + + let mut queue: BinaryHeap = BinaryHeap::new(); + dist[start_ind] = 0; + queue.push(HeapData { + cost: 0, + ind: start_ind, + }); + + // cant get stuck in a loop as cost for a distance either decreases or queue shrinks + while !queue.is_empty() { + let next = queue.pop().expect("would not be in while loop"); + if next.ind == end_ind { + break; } - dist[start_ind] = 0; - queue.push(HeapData { - cost: 0, - ind: start_ind, - }); + // the node is reached by faster means so already popped + if next.cost > dist[next.ind] { + continue; + } - // cant get stuck in a loop as cost for a distance either decreases or queue shrinks - while !queue.is_empty() { - let next = queue.pop().expect("would not be in while loop"); - if next.ind == end_ind { - break; + for i in 0..edge_set[next.ind].len() { + let edge_cost = edge_costs[next.ind][i]; + if dist[next.ind] + edge_cost < dist[edge_set[next.ind][i]] { + dist[edge_set[next.ind][i]] = dist[next.ind] + edge_cost; + queue.push(HeapData { + cost: dist[edge_set[next.ind][i]], + ind: edge_set[next.ind][i], + }); } + } + } - // the node is reached by faster means so already popped - if next.cost > dist[next.ind] { - continue; - } + dist + } - for i in 0..edge_set[next.ind].len() { - let edge_cost = edge_costs[next.ind][i]; - if dist[next.ind] + edge_cost < dist[edge_set[next.ind][i]] { - dist[edge_set[next.ind][i]] = dist[next.ind] + edge_cost; - queue.push(HeapData { - cost: dist[edge_set[next.ind][i]], - ind: edge_set[next.ind][i], - }); - } + fn render_match_corner_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 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 + } + + fn render_match_corner( + &self, + ratio_offset: f32, + start_abs_offset: f32, + end_abs_offset: f32, + sel_bb: BoundingBox, + eel_bb: BoundingBox, + abs_offset_set: bool, + ) -> Result> { + let (x1, y1) = self.start.origin; + let (x2, y2) = self.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)) = (self.start.dir, self.end.dir) { + // x_lines have constant x vary over y + let (x_lines, y_lines, mid_x, mid_y) = self.render_match_corner_get_lines( + ratio_offset, + (start_abs_offset, end_abs_offset), + (sel_bb, eel_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)); } } - for i in (0..back_points_inds.len()).rev() { - points.push(point_set[back_points_inds[i]]); - } + let mut edge_set = Self::render_match_corner_get_edges(&point_set, sel_bb, eel_bb); + let (start_ind, end_ind) = self.render_match_corner_add_start_and_end( + &mut point_set, + &mut edge_set, + sel_bb, + eel_bb, + start_dir_some, + end_dir_some, + ); + + let edge_costs = Self::render_match_corner_cost_function( + &point_set, &edge_set, x_lines, y_lines, mid_x, mid_y, + ); + + let dist = Self::render_match_corner_dijkstra_get_dists( + &point_set, + &edge_set, + &edge_costs, + start_ind, + end_ind, + ); + + points = Self::render_match_corner_dijkstra_get_points( + dist, + &point_set, + &edge_set, + &edge_costs, + start_ind, + end_ind, + ); } else { points = vec![(x1, y1), (x2, y2)]; } - return Ok(points); + Ok(points) } pub fn render(&self, ctx: &impl ElementMap) -> Result { @@ -1282,7 +1272,7 @@ impl Connector { eel_bb = el_bb; } } - let points = self.render_match_conner( + let points = self.render_match_corner( ratio_offset, start_abs_offset, end_abs_offset, @@ -1291,20 +1281,17 @@ impl Connector { abs_offset_set, )?; - - // TODO: remove repeated points. - if self.corner_radius != 0.0{ + if self.corner_radius != 0.0 { SvgElement::new( "path", &[( "d".to_string(), - Self::points_to_path(points,self.corner_radius) + Self::points_to_path(points, self.corner_radius), )], ) .with_attrs_from(&self.source_element) - } - else if points.len() == 2 { + } else if points.len() == 2 { SvgElement::new( "line", &[ @@ -1334,17 +1321,19 @@ impl Connector { Ok(conn_element) } - fn points_to_path(points: Vec<(f32,f32)>, max_radius: f32) -> String{ + fn points_to_path(points: Vec<(f32, f32)>, max_radius: f32) -> String { let mut result = String::new(); let mut radii = vec![]; - for i in 1..(points.len()-1){ - let mut d1 = (points[i].0-points[i-1].0).abs() + (points[i].1-points[i-1].1).abs(); - let mut d2 = (points[i+1].0-points[i].0).abs() + (points[i+1].1-points[i].1).abs(); - if i != 1{ - d1 = d1/2.0; + for i in 1..(points.len() - 1) { + let mut d1 = + (points[i].0 - points[i - 1].0).abs() + (points[i].1 - points[i - 1].1).abs(); + let mut d2 = + (points[i + 1].0 - points[i].0).abs() + (points[i + 1].1 - points[i].1).abs(); + if i != 1 { + d1 /= 2.0; } - if i != points.len()-2{ - d2 = d2/2.0; + if i != points.len() - 2 { + d2 /= 2.0; } let radius = d1.min(d2).min(max_radius); radii.push(radius); @@ -1353,33 +1342,42 @@ impl Connector { let mut pos = points[0]; result += &("M ".to_owned() + &pos.0.to_string() + "," + &pos.1.to_string() + "\n"); - for i in 1..(points.len()-1){ - let dx1 = points[i].0-pos.0; - let dy1 = points[i].1-pos.1; - let dx2 = points[i+1].0-points[i].0; - let dy2 = points[i+1].1-points[i].1; - - pos.0 = pos.0 + dx1 - dx1*radii[i-1]/(dx1*dx1+dy1*dy1).sqrt(); - pos.1 = pos.1 + dy1 - dy1*radii[i-1]/(dx1*dx1+dy1*dy1).sqrt(); - + for i in 1..(points.len() - 1) { + let dx1 = points[i].0 - pos.0; + let dy1 = points[i].1 - pos.1; + let dx2 = points[i + 1].0 - points[i].0; + let dy2 = points[i + 1].1 - points[i].1; + + pos.0 += dx1 - dx1 * radii[i - 1] / (dx1 * dx1 + dy1 * dy1).sqrt(); + pos.1 += dy1 - dy1 * radii[i - 1] / (dx1 * dx1 + dy1 * dy1).sqrt(); + result += &("L ".to_owned() + &pos.0.to_string() + "," + &pos.1.to_string() + "\n"); let mut new_pos = points[i]; - - new_pos.0 = new_pos.0 + dx2*radii[i-1]/(dx2*dx2+dy2*dy2).sqrt(); - new_pos.1 = new_pos.1 + dy2*radii[i-1]/(dx2*dx2+dy2*dy2).sqrt(); - let cl = (dx1*dy2 - dy1*dx2) > 0.0; - let cl_str = if cl {"1"} else{"0"}; + new_pos.0 += dx2 * radii[i - 1] / (dx2 * dx2 + dy2 * dy2).sqrt(); + new_pos.1 += dy2 * radii[i - 1] / (dx2 * dx2 + dy2 * dy2).sqrt(); - result += &("a ".to_owned() + &radii[i-1].to_string() + "," + &radii[i-1].to_string() + " 0 0 " + cl_str + " " + &(new_pos.0-pos.0).to_string() + "," + &(new_pos.1-pos.1).to_string() + "\n"); + let cl = (dx1 * dy2 - dy1 * dx2) > 0.0; + let cl_str = if cl { "1" } else { "0" }; - pos = new_pos; + result += &("a ".to_owned() + + &radii[i - 1].to_string() + + "," + + &radii[i - 1].to_string() + + " 0 0 " + + cl_str + + " " + + &(new_pos.0 - pos.0).to_string() + + "," + + &(new_pos.1 - pos.1).to_string() + + "\n"); + pos = new_pos; } - pos = points[points.len()-1]; + pos = points[points.len() - 1]; result += &("L ".to_owned() + &pos.0.to_string() + "," + &pos.1.to_string() + "\n"); - return result; + result } } diff --git a/src/elements/layout.rs b/src/elements/layout.rs index afc4456..00cf043 100644 --- a/src/elements/layout.rs +++ b/src/elements/layout.rs @@ -893,7 +893,7 @@ fn eval_text_anchor(element: &mut SvgElement, ctx: &impl ContextView) -> Result< LocSpec::BottomEdge(_) => element.set_default_attr("text-loc", "b"), LocSpec::LeftEdge(_) => element.set_default_attr("text-loc", "l"), LocSpec::RightEdge(_) => element.set_default_attr("text-loc", "r"), - LocSpec::PureLength(_) => element.set_default_attr("text-loc", "c"),// not sure + LocSpec::PureLength(_) => element.set_default_attr("text-loc", "c"), // not sure } } else { return Err(SvgdxError::InvalidData(format!( diff --git a/src/geometry/bbox.rs b/src/geometry/bbox.rs index e4d6d3a..2ac2f28 100644 --- a/src/geometry/bbox.rs +++ b/src/geometry/bbox.rs @@ -65,7 +65,7 @@ impl BoundingBox { RightEdge(len) => (self.x2, len.calc_offset(self.y1, self.y2)), BottomEdge(len) => (len.calc_offset(self.x1, self.x2), self.y2), LeftEdge(len) => (self.x1, len.calc_offset(self.y1, self.y2)), - PureLength(len) => panic!(), + PureLength(_) => panic!(), } } From 6f0830f4bcbec0f87f170487e30240da0115c0d0 Mon Sep 17 00:00:00 2001 From: megamaths Date: Fri, 8 Aug 2025 16:20:30 +0100 Subject: [PATCH 5/8] added tests for point along line, removed some potential crashes and tidied up connector a bit --- src/elements/connector.rs | 611 ++++++++---------------------------- src/elements/layout.rs | 39 ++- src/elements/line_offset.rs | 571 +++++++++++++++++++++++++++++++++ src/elements/mod.rs | 1 + src/elements/text.rs | 17 +- src/geometry/bbox.rs | 30 +- src/geometry/types.rs | 36 ++- src/transform.rs | 4 +- 8 files changed, 773 insertions(+), 536 deletions(-) create mode 100644 src/elements/line_offset.rs diff --git a/src/elements/connector.rs b/src/elements/connector.rs index 474564a..6546970 100644 --- a/src/elements/connector.rs +++ b/src/elements/connector.rs @@ -3,6 +3,7 @@ use std::collections::BinaryHeap; use super::SvgElement; use crate::context::ElementMap; +use crate::elements::line_offset::get_point_along_linelike_type_el; use crate::errors::{Result, SvgdxError}; use crate::geometry::{parse_el_loc, strp_length, BoundingBox, Length, LocSpec, ScalarSpec}; use crate::types::{attr_split, fstr, strp}; @@ -95,7 +96,9 @@ fn closest_loc( .ok_or_else(|| SvgdxError::MissingBoundingBox(this.to_string()))?; for loc in edge_locations(conn_type) { - let this_coord = this_bb.locspec(loc); + let this_coord = this_bb + .locspec(loc) + .expect("always some as LineOffset not in edge_locations"); let ((x1, y1), (x2, y2)) = (this_coord, point); let dist_sq = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); if dist_sq < min_dist_sq { @@ -125,8 +128,13 @@ fn shortest_link( for this_loc in edge_locations(conn_type) { for that_loc in edge_locations(conn_type) { - let this_coord = this_bb.locspec(this_loc); - let that_coord = that_bb.locspec(that_loc); + let this_coord = this_bb + .locspec(this_loc) + .expect("always some as LineOffset not in edge_locations"); + let that_coord = that_bb + .locspec(that_loc) + .expect("always some as LineOffset not in edge_locations"); + // 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 { @@ -139,14 +147,14 @@ fn shortest_link( Ok((this_min_loc, that_min_loc)) } +// from the exaple heap docs https://doc.rust-lang.org/std/collections/binary_heap/index.html #[derive(PartialEq, Eq)] -struct HeapData { +struct PathCost { cost: u32, - ind: usize, + idx: usize, } -impl Ord for HeapData { - // from the exaple heap docs +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 @@ -154,11 +162,11 @@ impl Ord for HeapData { other .cost .cmp(&self.cost) - .then_with(|| self.ind.cmp(&other.ind)) + .then_with(|| self.idx.cmp(&other.idx)) } } -impl PartialOrd for HeapData { +impl PartialOrd for PathCost { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } @@ -184,12 +192,11 @@ impl Connector { fn parse_element<'a>( element: &mut SvgElement, elem_map: &'a impl ElementMap, - start: bool, + attr_name: &str, ) -> Result> { - let attrib_name = if start { "start" } else { "end" }; let this_ref = element - .pop_attr(attrib_name) - .ok_or_else(|| SvgdxError::MissingAttribute(attrib_name.to_string()))?; + .pop_attr(attr_name) + .ok_or_else(|| SvgdxError::MissingAttribute(attr_name.to_string()))?; let mut ret = ElementParseData { el: None, @@ -210,12 +217,12 @@ impl Connector { ret.point = Some(( parts.next().ok_or_else(|| { SvgdxError::InvalidData( - (attrib_name.to_owned() + "_ref x should be numeric").to_owned(), + (attr_name.to_owned() + "_ref x should be numeric").to_owned(), ) })?, parts.next().ok_or_else(|| { SvgdxError::InvalidData( - (attrib_name.to_owned() + "_ref y should be numeric").to_owned(), + (attr_name.to_owned() + "_ref y should be numeric").to_owned(), ) })?, )); @@ -224,399 +231,20 @@ impl Connector { Ok(ret) } - fn get_point_along_line(el: &SvgElement, length: Length) -> Result<(f32, f32)> { - let is_percent; - let dist; - match length { - Length::Absolute(abs) => { - dist = abs; - is_percent = false; - } - Length::Ratio(rat) => { - dist = rat; - is_percent = true; - } - } - - 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 = x1.parse()?; - let y1: f32 = y1.parse()?; - let x2: f32 = x2.parse()?; - let y2: f32 = y2.parse()?; - if x1 == x2 && y1 == y2 { - return Ok((x1, y1)); - } - - let rat = if is_percent { - dist - } else { - let len = ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)).sqrt(); - dist / len - }; - return Ok((x1 + rat * (x2 - x1), y1 + rat * (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 mut is_percent; - let mut dist; - match length { - Length::Absolute(abs) => { - dist = abs; - is_percent = false; - } - Length::Ratio(rat) => { - dist = rat; - is_percent = true; - } - } - - if let Some(points) = el.get_attr("points") { - let points = points.split(", "); - let mut lastx; - let mut lasty; - - // loop to allow repeat to find total length if a percentage - loop { - let mut cummulative_dist = 0.0; - lastx = 0.0; - lasty = 0.0; - let mut first_point = true; - for p in points.clone() { - let mut this_point = p.split_whitespace(); - if let (Some(x), Some(y)) = (this_point.next(), this_point.next()) { - let x: f32 = x.parse()?; - let y: f32 = y.parse()?; - - if !first_point { - let len = - ((lastx - x) * (lastx - x) + (lasty - y) * (lasty - y)).sqrt(); - if !is_percent && cummulative_dist + len > dist { - let rat = (dist - cummulative_dist) / len; - return Ok(( - lastx * (1.0 - rat) + rat * x, - lasty * (1.0 - rat) + rat * y, - )); - } - cummulative_dist += len; - } else if dist < 0.0 { - // clamp to start - return Ok((x, y)); - } - lastx = x; - lasty = y; - } - - first_point = false; - } - if !is_percent { - break; - } else { - is_percent = false; - dist *= cummulative_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 mut is_percent; - let mut dist; - match length { - Length::Absolute(abs) => { - dist = abs; - is_percent = false; - } - Length::Ratio(rat) => { - dist = rat; - is_percent = true; - } - } - - if let Some(d) = el.get_attr("d") { - let replaced_commas = d.replace([','], " "); - let items = replaced_commas.split_whitespace(); - - let mut pos; - // loop to allow repeat to find total length if a percentage - loop { - let mut cummulative_dist = 0.0; - pos = (0.0, 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 items.clone() { - if item.starts_with([ - 'a', 'A', '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 { - if ['c', 'C', 'q', 'Q', 's', 'S', 't', 'T', 'z', 'Z'].contains(&op) { - todo!("not yet impl path parsing"); - } else if op == 'm' { - if arg_num == 0 { - pos.0 += item.parse::()?; - } else if arg_num == 1 { - pos.1 += item.parse::()?; - last_stable_pos = pos; - } else { - return Err(SvgdxError::ParseError( - "path has too many vars".to_string(), - )); - } - } else if op == 'M' { - if arg_num == 0 { - pos.0 = item.parse::()?; - } else if arg_num == 1 { - pos.1 = item.parse::()?; - last_stable_pos = pos; - } else { - return Err(SvgdxError::ParseError( - "path has too many vars".to_string(), - )); - } - } else if op == 'h' || op == 'H' { - if arg_num == 0 { - let val = item.parse::()?; - if op == 'h' { - pos.0 += val; - } else { - pos.0 = val; - } - let d = (pos.0 - last_stable_pos.0).abs(); - if !is_percent && cummulative_dist + d > dist { - let r = (dist - cummulative_dist) / d; - return Ok((last_stable_pos.0 * (1.0 - r) + pos.0 * r, pos.1)); - } - - cummulative_dist += d; - last_stable_pos = pos; - } else { - return Err(SvgdxError::ParseError( - "path has too many vars".to_string(), - )); - } - } else if op == 'v' || op == 'V' { - if arg_num == 0 { - let val = item.parse::()?; - if op == 'v' { - pos.1 += val; - } else { - pos.1 = val; - } - let d = (pos.1 - last_stable_pos.1).abs(); - if !is_percent && cummulative_dist + d > dist { - let r = (dist - cummulative_dist) / d; - return Ok((pos.0, last_stable_pos.1 * (1.0 - r) + pos.1 * r)); - } - - cummulative_dist += d; - last_stable_pos = pos; - } else { - return Err(SvgdxError::ParseError( - "path has too many vars".to_string(), - )); - } - } else if op == 'l' || op == 'L' { - if arg_num == 0 { - let val = item.parse::()?; - if op == 'l' { - pos.0 += val; - } else { - pos.0 = val; - } - } else if arg_num == 1 { - let val = item.parse::()?; - if op == 'l' { - pos.1 += val; - } else { - pos.1 = val; - } - let d = ((last_stable_pos.0 - pos.0) * (last_stable_pos.0 - pos.0) - + (last_stable_pos.1 - pos.1) * (last_stable_pos.1 - pos.1)) - .sqrt(); - if !is_percent && cummulative_dist + d > dist { - let r = (dist - cummulative_dist) / d; - return Ok(( - last_stable_pos.0 * (1.0 - r) + pos.0 * r, - last_stable_pos.1 * (1.0 - r) + pos.1 * r, - )); - } - - cummulative_dist += d; - last_stable_pos = pos; - } else { - return Err(SvgdxError::ParseError( - "path has too many vars".to_string(), - )); - } - } else if op == 'a' || op == 'A' { - if arg_num == 0 { - let val = item.parse::()?; - r = val; - } else if arg_num == 1 { - let val = item.parse::()?; - 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 = item.parse::()?; - if op == 'a' { - pos.0 += val; - } else { - pos.0 = val; - } - } else if arg_num == 6 { - let val = item.parse::()?; - 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 { - 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; - } - 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 && cummulative_dist + arc_length > dist { - let ratio = (dist - cummulative_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(), - )); - } - - cummulative_dist += arc_length; - last_stable_pos = pos; - } else { - return Err(SvgdxError::ParseError( - "path has too many vars".to_string(), - )); - } - } - - arg_num += 1; - } - } - if !is_percent { - break; - } else { - is_percent = false; - dist *= cummulative_dist; - } - } - return Ok(pos); - } - - Err(SvgdxError::MissingAttribute("d in path".to_string())) - } - - fn get_point_along_linish_type_el(el: &SvgElement, length: Length) -> Result<(f32, f32)> { - let name = el.name(); - - if name == "line" { - return Self::get_point_along_line(el, length); - } - if name == "polyline" { - return Self::get_point_along_polyline(el, length); - } - if name == "path" { - return Self::get_point_along_path(el, length); - } - - Err(SvgdxError::MissingAttribute( - "looking for point on line in a non line element".to_string(), - )) - } - fn get_coord_element_loc( elem_map: &impl ElementMap, el: &SvgElement, loc: LocSpec, ) -> Result<(f32, f32)> { - if let LocSpec::PureLength(l) = loc { - return Self::get_point_along_linish_type_el(el, l); + if let LocSpec::LineOffset(l) = loc { + return get_point_along_linelike_type_el(el, l); } let coord = elem_map .get_element_bbox(el)? .ok_or_else(|| SvgdxError::MissingBoundingBox(el.to_string()))? - .locspec(loc); + .locspec(loc) + .expect("only None if LineOffset and know is not"); Ok(coord) } @@ -628,8 +256,8 @@ impl Connector { ) -> Result { let mut element = element.clone(); - let start_ret = Self::parse_element(&mut element, elem_map, true)?; - let end_ret = Self::parse_element(&mut element, elem_map, false)?; + 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, start_point, mut start_dir) = (start_ret.el, start_ret.loc, start_ret.point, start_ret.dir); let (end_el, mut end_loc, end_point, mut end_dir) = @@ -750,6 +378,8 @@ impl Connector { }) } + // 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 { @@ -780,45 +410,42 @@ impl Connector { ) -> (Vec, Vec, usize, usize) { let (x1, y1) = self.start.origin; let (x2, y2) = self.end.origin; - let sel_bb = bbs.0; - let eel_bb = bbs.1; - let start_abs_offset = abs_offsets.0; - let end_abs_offset = abs_offsets.1; - let start_dir = dirs.0; - let end_dir = dirs.1; + 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(sel_bb.x1 - start_abs_offset); - x_lines.push(sel_bb.x2 + start_abs_offset); - x_lines.push(eel_bb.x1 - end_abs_offset); - x_lines.push(eel_bb.x2 + end_abs_offset); + 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 sel_bb.x1 > eel_bb.x2 { + if start_el_bb.x1 > end_el_bb.x2 { // there is a gap - x_lines.push((sel_bb.x1 + eel_bb.x2) * 0.5); + x_lines.push((start_el_bb.x1 + end_el_bb.x2) * 0.5); mid_x = x_lines.len() - 1; - } else if sel_bb.x2 < eel_bb.x1 { + } else if start_el_bb.x2 < end_el_bb.x1 { // there is a gap - x_lines.push((sel_bb.x2 + eel_bb.x1) * 0.5); + x_lines.push((start_el_bb.x2 + end_el_bb.x1) * 0.5); mid_x = x_lines.len() - 1; } - y_lines.push(sel_bb.y1 - start_abs_offset); - y_lines.push(sel_bb.y2 + start_abs_offset); - y_lines.push(eel_bb.y1 - end_abs_offset); - y_lines.push(eel_bb.y2 + end_abs_offset); + 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 sel_bb.y1 > eel_bb.y2 { + if start_el_bb.y1 > end_el_bb.y2 { // there is a gap - y_lines.push(sel_bb.y1 * (1.0 - ratio_offset) + eel_bb.y2 * ratio_offset); + 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 sel_bb.y2 < eel_bb.y1 { + } else if start_el_bb.y2 < end_el_bb.y1 { // there is a gap - y_lines.push(sel_bb.y2 * (1.0 - ratio_offset) + eel_bb.y1 * ratio_offset); + y_lines.push(start_el_bb.y2 * (1.0 - ratio_offset) + end_el_bb.y1 * ratio_offset); mid_y = y_lines.len() - 1; } @@ -842,10 +469,10 @@ impl Connector { if abs_offset_set { match start_dir { - Direction::Down => mid_y = 1, // positive x - Direction::Left => mid_x = 0, // negative y - Direction::Right => mid_x = 1, // positive y - Direction::Up => mid_y = 0, // positive x + 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 } } @@ -854,8 +481,8 @@ impl Connector { fn render_match_corner_get_edges( point_set: &[(f32, f32)], - sel_bb: BoundingBox, - eel_bb: BoundingBox, + start_el_bb: BoundingBox, + end_el_bb: BoundingBox, ) -> Vec> { let mut edge_set = vec![vec![]; point_set.len()]; @@ -869,14 +496,14 @@ impl Connector { // check if not blocked by a wall if point_set[i].0 == point_set[j].0 && !Self::aals_blocked_by_bb( - sel_bb, + start_el_bb, point_set[i].1, point_set[j].1, false, point_set[i].0, ) && !Self::aals_blocked_by_bb( - eel_bb, + end_el_bb, point_set[i].1, point_set[j].1, false, @@ -887,14 +514,14 @@ impl Connector { } if point_set[i].1 == point_set[j].1 && !Self::aals_blocked_by_bb( - sel_bb, + start_el_bb, point_set[i].0, point_set[j].0, true, point_set[i].1, ) && !Self::aals_blocked_by_bb( - eel_bb, + end_el_bb, point_set[i].0, point_set[j].0, true, @@ -917,8 +544,8 @@ impl Connector { &self, point_set: &mut Vec<(f32, f32)>, edge_set: &mut Vec>, - sel_bb: BoundingBox, - eel_bb: BoundingBox, + start_el_bb: BoundingBox, + end_el_bb: BoundingBox, start_dir: Direction, end_dir: Direction, ) -> (usize, usize) { @@ -936,7 +563,7 @@ impl Connector { if point_set[i].0 == x1 && ((point_set[i].1 < y1 && start_dir == Direction::Up) || (point_set[i].1 > y1 && start_dir == Direction::Down)) - && !Self::aals_blocked_by_bb(eel_bb, point_set[i].1, y1, false, x1) + && !Self::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); @@ -944,7 +571,7 @@ impl Connector { if point_set[i].1 == y1 && ((point_set[i].0 > x1 && start_dir == Direction::Right) || (point_set[i].0 < x1 && start_dir == Direction::Left)) - && !Self::aals_blocked_by_bb(eel_bb, point_set[i].0, x1, true, y1) + && !Self::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); @@ -953,7 +580,7 @@ impl Connector { if point_set[i].0 == x2 && ((point_set[i].1 < y2 && end_dir == Direction::Up) || (point_set[i].1 > y2 && end_dir == Direction::Down)) - && !Self::aals_blocked_by_bb(sel_bb, point_set[i].1, y2, false, x2) + && !Self::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); @@ -961,7 +588,7 @@ impl Connector { if point_set[i].1 == y2 && ((point_set[i].0 > x2 && end_dir == Direction::Right) || (point_set[i].0 < x2 && end_dir == Direction::Left)) - && !Self::aals_blocked_by_bb(sel_bb, point_set[i].0, x2, true, y2) + && !Self::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); @@ -978,10 +605,12 @@ impl Connector { y_lines: Vec, mid_x: usize, mid_y: usize, + total_bb_size: u32, ) -> Vec> { // edge cost function - let corner_cost = 1000; + // needs to be comparable to or larger than total_bb_size + let corner_cost = 1000 + 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() { @@ -1019,37 +648,37 @@ impl Connector { edge_costs: &[Vec], start_ind: usize, end_ind: usize, + total_bb_size: u32, ) -> Vec { // just needs to be bigger than 5* (corner cost + total bounding box size) - let inf = 1000000; + let inf = 1000000 + 10 * total_bb_size; let mut dist = vec![inf; point_set.len()]; - let mut queue: BinaryHeap = BinaryHeap::new(); + let mut queue: BinaryHeap = BinaryHeap::new(); dist[start_ind] = 0; - queue.push(HeapData { + queue.push(PathCost { cost: 0, - ind: start_ind, + idx: start_ind, }); // cant get stuck in a loop as cost for a distance either decreases or queue shrinks - while !queue.is_empty() { - let next = queue.pop().expect("would not be in while loop"); - if next.ind == end_ind { + 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.ind] { + if next.cost > dist[next.idx] { continue; } - for i in 0..edge_set[next.ind].len() { - let edge_cost = edge_costs[next.ind][i]; - if dist[next.ind] + edge_cost < dist[edge_set[next.ind][i]] { - dist[edge_set[next.ind][i]] = dist[next.ind] + edge_cost; - queue.push(HeapData { - cost: dist[edge_set[next.ind][i]], - ind: edge_set[next.ind][i], + 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], }); } } @@ -1096,8 +725,8 @@ impl Connector { ratio_offset: f32, start_abs_offset: f32, end_abs_offset: f32, - sel_bb: BoundingBox, - eel_bb: BoundingBox, + start_el_bb: BoundingBox, + end_el_bb: BoundingBox, abs_offset_set: bool, ) -> Result> { let (x1, y1) = self.start.origin; @@ -1111,7 +740,7 @@ impl Connector { let (x_lines, y_lines, mid_x, mid_y) = self.render_match_corner_get_lines( ratio_offset, (start_abs_offset, end_abs_offset), - (sel_bb, eel_bb), + (start_el_bb, end_el_bb), abs_offset_set, (start_dir_some, end_dir_some), ); @@ -1123,18 +752,28 @@ impl Connector { } } - let mut edge_set = Self::render_match_corner_get_edges(&point_set, sel_bb, eel_bb); + let mut edge_set = + Self::render_match_corner_get_edges(&point_set, start_el_bb, end_el_bb); let (start_ind, end_ind) = self.render_match_corner_add_start_and_end( &mut point_set, &mut edge_set, - sel_bb, - eel_bb, + 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()) as u32; + let edge_costs = Self::render_match_corner_cost_function( - &point_set, &edge_set, x_lines, y_lines, mid_x, mid_y, + &point_set, + &edge_set, + x_lines, + y_lines, + mid_x, + mid_y, + total_bb_size, ); let dist = Self::render_match_corner_dijkstra_get_dists( @@ -1143,6 +782,7 @@ impl Connector { &edge_costs, start_ind, end_ind, + total_bb_size, ); points = Self::render_match_corner_dijkstra_get_points( @@ -1246,9 +886,13 @@ impl Connector { .with_attrs_from(&self.source_element), ConnectionType::Corner => { let mut abs_offset_set = false; - let mut start_abs_offset = default_abs_offset.absolute().ok_or("blarg 13872199")?; + 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().ok_or("blarg 13872198")?; + 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; @@ -1260,24 +904,24 @@ impl Connector { } } - let mut sel_bb = BoundingBox::new(x1, y1, x1, y1); - let mut eel_bb = BoundingBox::new(x2, y2, 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() { - sel_bb = el_bb; + start_el_bb = el_bb; } } if let Some(el) = &self.end_el { if let Ok(Some(el_bb)) = el.bbox() { - eel_bb = el_bb; + end_el_bb = el_bb; } } let points = self.render_match_corner( ratio_offset, start_abs_offset, end_abs_offset, - sel_bb, - eel_bb, + start_el_bb, + end_el_bb, abs_offset_set, )?; @@ -1322,7 +966,7 @@ impl Connector { } fn points_to_path(points: Vec<(f32, f32)>, max_radius: f32) -> String { - let mut result = String::new(); + let mut result = vec![]; let mut radii = vec![]; for i in 1..(points.len() - 1) { let mut d1 = @@ -1340,7 +984,7 @@ impl Connector { } let mut pos = points[0]; - result += &("M ".to_owned() + &pos.0.to_string() + "," + &pos.1.to_string() + "\n"); + result.push(format!("M {} {}", pos.0, pos.1)); for i in 1..(points.len() - 1) { let dx1 = points[i].0 - pos.0; @@ -1351,7 +995,7 @@ impl Connector { pos.0 += dx1 - dx1 * radii[i - 1] / (dx1 * dx1 + dy1 * dy1).sqrt(); pos.1 += dy1 - dy1 * radii[i - 1] / (dx1 * dx1 + dy1 * dy1).sqrt(); - result += &("L ".to_owned() + &pos.0.to_string() + "," + &pos.1.to_string() + "\n"); + result.push(format!("L {} {}", pos.0, pos.1)); let mut new_pos = points[i]; @@ -1361,23 +1005,20 @@ impl Connector { let cl = (dx1 * dy2 - dy1 * dx2) > 0.0; let cl_str = if cl { "1" } else { "0" }; - result += &("a ".to_owned() - + &radii[i - 1].to_string() - + "," - + &radii[i - 1].to_string() - + " 0 0 " - + cl_str - + " " - + &(new_pos.0 - pos.0).to_string() - + "," - + &(new_pos.1 - pos.1).to_string() - + "\n"); + result.push(format!( + "a {} {} 0 0 {} {} {}", + radii[i - 1], + radii[i - 1], + cl_str, + (new_pos.0 - pos.0), + (new_pos.1 - pos.1) + )); pos = new_pos; } pos = points[points.len() - 1]; - result += &("L ".to_owned() + &pos.0.to_string() + "," + &pos.1.to_string() + "\n"); + result.push(format!("L {} {}", pos.0, pos.1)); - result + result.as_slice().join(" ") } } diff --git a/src/elements/layout.rs b/src/elements/layout.rs index 00cf043..450af1a 100644 --- a/src/elements/layout.rs +++ b/src/elements/layout.rs @@ -78,11 +78,11 @@ fn expand_single_relspec(value: &str, ctx: &impl ElementMap) -> String { }; if let Ok((Some(elem), rest)) = split_relspec(value, ctx) { if rest.is_empty() && elem.name() == "point" { - if let Ok(Some(point)) = elem_loc(elem, LocSpec::Center) { + if let Ok(Some(Some(point))) = elem_loc(elem, LocSpec::Center) { return format!("{} {}", fstr(point.0), fstr(point.1)); } } else if let Some(loc) = rest.strip_prefix(LOCSPEC_SEP).and_then(|s| s.parse().ok()) { - if let Ok(Some(point)) = elem_loc(elem, loc) { + if let Ok(Some(Some(point))) = elem_loc(elem, loc) { return format!("{} {}", fstr(point.0), fstr(point.1)); } } else if let Some(scalar) = rest @@ -645,7 +645,9 @@ impl SvgElement { } else { 0. }; - let (x, y) = bbox.locspec(rel.to_locspec()); + let (x, y) = bbox + .locspec(rel.to_locspec()) + .expect("rel is not lineoffset and using non lineoffset garenteed not to be none"); let (dx, dy) = match rel { DirSpec::Above => (-this_width / 2., -(this_height + gap)), DirSpec::Below => (-this_width / 2., gap), @@ -658,7 +660,9 @@ impl SvgElement { // Need to determine top-left corner of the target bbox which // may not be (0, 0), and offset by the equivalent amount. if let Some(bbox) = ctx.get_target_element(self)?.bbox()? { - let (tx, ty) = bbox.locspec(LocSpec::TopLeft); + let (tx, ty) = bbox + .locspec(LocSpec::TopLeft) + .expect("using non lineoffset garenteed not to be none"); pos.xmin = Some(x + dx - tx); pos.ymin = Some(y + dy - ty); } @@ -679,7 +683,9 @@ fn position_from_bbox(element: &mut SvgElement, bb: &BoundingBox, inscribe: bool let width = bb.width(); let height = bb.height(); let (cx, cy) = bb.center(); - let (x1, y1) = bb.locspec(LocSpec::TopLeft); + let (x1, y1) = bb + .locspec(LocSpec::TopLeft) + .expect("using non lineoffset garenteed not to be none"); match element.name() { "rect" | "box" => { element.set_attr("x", &fstr(x1)); @@ -824,14 +830,19 @@ fn pos_attr_helper( "Could not parse '{loc_str}' in this context", ))); } - let (x, y) = bbox.locspec(loc); - let (dx, dy) = extract_dx_dy(dxy)?; - use ScalarSpec::*; - v = match attr_ss { - Minx | Maxx | Cx => x + dx, - Miny | Maxy | Cy => y + dy, - _ => v, - }; + if let Some((x, y)) = bbox.locspec(loc) { + let (dx, dy) = extract_dx_dy(dxy)?; + use ScalarSpec::*; + v = match attr_ss { + Minx | Maxx | Cx => x + dx, + Miny | Maxy | Cy => y + dy, + _ => v, + }; + } else { + return Err(SvgdxError::InvalidData( + "general use of lineoffset not yet supported".to_string(), + )); + } } Ok(fstr(v).to_string()) } @@ -893,7 +904,7 @@ fn eval_text_anchor(element: &mut SvgElement, ctx: &impl ContextView) -> Result< LocSpec::BottomEdge(_) => element.set_default_attr("text-loc", "b"), LocSpec::LeftEdge(_) => element.set_default_attr("text-loc", "l"), LocSpec::RightEdge(_) => element.set_default_attr("text-loc", "r"), - LocSpec::PureLength(_) => element.set_default_attr("text-loc", "c"), // not sure + LocSpec::LineOffset(_) => element.set_default_attr("text-loc", "c"), } } else { return Err(SvgdxError::InvalidData(format!( diff --git a/src/elements/line_offset.rs b/src/elements/line_offset.rs new file mode 100644 index 0000000..3cd7d00 --- /dev/null +++ b/src/elements/line_offset.rs @@ -0,0 +1,571 @@ +use super::SvgElement; +use crate::errors::{Result, SvgdxError}; +use crate::geometry::Length; + +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 = x1.parse()?; + let y1: f32 = y1.parse()?; + let x2: f32 = x2.parse()?; + let y2: f32 = y2.parse()?; + if x1 == x2 && y1 == y2 { + return Ok((x1, y1)); + } + + let ratio = if is_percent { + dist + } else { + let len = ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)).sqrt(); + 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 (mut is_percent, mut dist) = match length { + Length::Absolute(abs) => (false, abs), + Length::Ratio(ratio) => (true, ratio), + }; + + if let Some(points) = el.get_attr("points") { + let replaced_commas = points.replace([','], " "); + let mut lastx; + let mut lasty; + + // loop to allow repeat to find total length if a percentage + loop { + let mut points = replaced_commas.split_whitespace(); + let mut cummulative_dist = 0.0; + lastx = 0.0; + lasty = 0.0; + let mut first_point = true; + while let (Some(x), Some(y)) = (points.next(), points.next()) { + let x: f32 = x.parse()?; + let y: f32 = y.parse()?; + + if !first_point { + let len = ((lastx - x) * (lastx - x) + (lasty - y) * (lasty - y)).sqrt(); + if !is_percent && cummulative_dist + len > dist { + let ratio = (dist - cummulative_dist) / len; + return Ok(( + lastx * (1.0 - ratio) + ratio * x, + lasty * (1.0 - ratio) + ratio * y, + )); + } + cummulative_dist += len; + } else if dist < 0.0 { + // clamp to start + return Ok((x, y)); + } + lastx = x; + lasty = y; + + first_point = false; + } + if !is_percent { + break; + } else { + is_percent = false; + dist *= cummulative_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 (mut is_percent, mut dist) = match length { + Length::Absolute(abs) => (false, abs), + Length::Ratio(ratio) => (true, ratio), + }; + + if let Some(d) = el.get_attr("d") { + let replaced_commas = d.replace([','], " "); + let items = replaced_commas.split_whitespace(); + + let mut pos; + // loop to allow repeat to find total length if a percentage + loop { + let mut cummulative_dist = 0.0; + pos = (0.0, 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 items.clone() { + 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 { + if ['b', 'B', 'c', 'C', 'q', 'Q', 's', 'S', 't', 'T', 'z', 'Z'].contains(&op) { + return Err(SvgdxError::InvalidData(format!( + "not yet impl path parsing line offset for {op}" + ))); + } else if op == 'm' || op == 'M' { + if arg_num == 0 { + let val = item.parse::()?; + if op == 'm' { + pos.0 += val; + } else { + pos.0 = val; + } + } else if arg_num == 1 { + let val = item.parse::()?; + if op == 'm' { + pos.1 += val; + } else { + pos.1 = val; + } + last_stable_pos = pos; + } else { + return Err(SvgdxError::ParseError( + "path has too many vars".to_string(), + )); + } + } else if op == 'h' || op == 'H' { + if arg_num == 0 { + let val = item.parse::()?; + if op == 'h' { + pos.0 += val; + } else { + pos.0 = val; + } + let d = (pos.0 - last_stable_pos.0).abs(); + if !is_percent && cummulative_dist + d > dist { + let r = (dist - cummulative_dist) / d; + return Ok((last_stable_pos.0 * (1.0 - r) + pos.0 * r, pos.1)); + } + + cummulative_dist += d; + last_stable_pos = pos; + } else { + return Err(SvgdxError::ParseError( + "path has too many vars".to_string(), + )); + } + } else if op == 'v' || op == 'V' { + if arg_num == 0 { + let val = item.parse::()?; + if op == 'v' { + pos.1 += val; + } else { + pos.1 = val; + } + let d = (pos.1 - last_stable_pos.1).abs(); + if !is_percent && cummulative_dist + d > dist { + let r = (dist - cummulative_dist) / d; + return Ok((pos.0, last_stable_pos.1 * (1.0 - r) + pos.1 * r)); + } + + cummulative_dist += d; + last_stable_pos = pos; + } else { + return Err(SvgdxError::ParseError( + "path has too many vars".to_string(), + )); + } + } else if op == 'l' || op == 'L' { + if arg_num == 0 { + let val = item.parse::()?; + if op == 'l' { + pos.0 += val; + } else { + pos.0 = val; + } + } else if arg_num == 1 { + let val = item.parse::()?; + if op == 'l' { + pos.1 += val; + } else { + pos.1 = val; + } + let d = ((last_stable_pos.0 - pos.0) * (last_stable_pos.0 - pos.0) + + (last_stable_pos.1 - pos.1) * (last_stable_pos.1 - pos.1)) + .sqrt(); + if !is_percent && cummulative_dist + d > dist { + let r = (dist - cummulative_dist) / d; + return Ok(( + last_stable_pos.0 * (1.0 - r) + pos.0 * r, + last_stable_pos.1 * (1.0 - r) + pos.1 * r, + )); + } + + cummulative_dist += d; + last_stable_pos = pos; + } else { + return Err(SvgdxError::ParseError( + "path has too many vars".to_string(), + )); + } + } else if op == 'a' || op == 'A' { + if arg_num == 0 { + let val = item.parse::()?; + r = val; + } else if arg_num == 1 { + let val = item.parse::()?; + 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 = item.parse::()?; + if op == 'a' { + pos.0 += val; + } else { + pos.0 = val; + } + } else if arg_num == 6 { + let val = item.parse::()?; + 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 && cummulative_dist + arc_length > dist { + let ratio = (dist - cummulative_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(), + )); + } + + cummulative_dist += arc_length; + last_stable_pos = pos; + } else { + return Err(SvgdxError::ParseError( + "path has too many vars".to_string(), + )); + } + } + + arg_num += 1; + } + } + if !is_percent { + break; + } else { + is_percent = false; + dist *= cummulative_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)> { + let name = el.name(); + + if name == "line" { + return get_point_along_line(el, length); + } + if name == "polyline" { + return get_point_along_polyline(el, length); + } + if name == "path" { + return get_point_along_path(el, length); + } + + Err(SvgdxError::MissingAttribute( + "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..52b1fdd 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -3,6 +3,7 @@ mod connector; mod containers; 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..bb64789 100644 --- a/src/elements/text.rs +++ b/src/elements/text.rs @@ -140,14 +140,19 @@ fn get_text_position(element: &mut SvgElement) -> Result<(f32, f32, bool, LocSpe // Assumption is that text should be centered within the rect, // and has styling via CSS to reflect this, e.g.: // text.d-text { dominant-baseline: central; text-anchor: middle; } - let (mut tdx, mut tdy) = element + if let Some((mut tdx, mut tdy)) = element .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)) + .locspec(text_anchor) + { + tdx += t_dx; + tdy += t_dy; + Ok((tdx, tdy, outside, text_anchor, text_classes)) + } else { + Err(SvgdxError::InvalidData( + "trying to use lineoffset on text element not supported yet".to_string(), + )) + } } pub fn process_text_attr(element: &SvgElement) -> Result<(SvgElement, Vec)> { diff --git a/src/geometry/bbox.rs b/src/geometry/bbox.rs index 2ac2f28..1be77db 100644 --- a/src/geometry/bbox.rs +++ b/src/geometry/bbox.rs @@ -44,7 +44,7 @@ impl BoundingBox { Self { x1, y1, x2, y2 } } - pub fn locspec(&self, ls: LocSpec) -> (f32, f32) { + pub fn locspec(&self, ls: LocSpec) -> Option<(f32, f32)> { let tl = (self.x1, self.y1); let tr = (self.x2, self.y1); let br = (self.x2, self.y2); @@ -52,20 +52,20 @@ impl BoundingBox { let c = ((self.x1 + self.x2) / 2., (self.y1 + self.y2) / 2.); use LocSpec::*; match ls { - TopLeft => tl, - Top => ((self.x1 + self.x2) / 2., self.y1), - TopRight => tr, - Right => (self.x2, (self.y1 + self.y2) / 2.), - BottomRight => br, - Bottom => ((self.x1 + self.x2) / 2., self.y2), - BottomLeft => bl, - Left => (self.x1, (self.y1 + self.y2) / 2.), - Center => c, - TopEdge(len) => (len.calc_offset(self.x1, self.x2), self.y1), - RightEdge(len) => (self.x2, len.calc_offset(self.y1, self.y2)), - BottomEdge(len) => (len.calc_offset(self.x1, self.x2), self.y2), - LeftEdge(len) => (self.x1, len.calc_offset(self.y1, self.y2)), - PureLength(_) => panic!(), + TopLeft => Some(tl), + Top => Some(((self.x1 + self.x2) / 2., self.y1)), + TopRight => Some(tr), + Right => Some((self.x2, (self.y1 + self.y2) / 2.)), + BottomRight => Some(br), + Bottom => Some(((self.x1 + self.x2) / 2., self.y2)), + BottomLeft => Some(bl), + Left => Some((self.x1, (self.y1 + self.y2) / 2.)), + Center => Some(c), + TopEdge(len) => Some((len.calc_offset(self.x1, self.x2), self.y1)), + RightEdge(len) => Some((self.x2, len.calc_offset(self.y1, self.y2))), + BottomEdge(len) => Some((len.calc_offset(self.x1, self.x2), self.y2)), + LeftEdge(len) => Some((self.x1, len.calc_offset(self.y1, self.y2))), + LineOffset(_) => None, } } diff --git a/src/geometry/types.rs b/src/geometry/types.rs index f6716cf..82782c6 100644 --- a/src/geometry/types.rs +++ b/src/geometry/types.rs @@ -139,7 +139,7 @@ pub enum LocSpec { RightEdge(Length), BottomEdge(Length), LeftEdge(Length), - PureLength(Length), + LineOffset(Length), } impl LocSpec { @@ -194,7 +194,7 @@ impl FromStr for LocSpec { "r" => Ok(Self::RightEdge(len)), "b" => Ok(Self::BottomEdge(len)), "l" => Ok(Self::LeftEdge(len)), - "" => Ok(Self::PureLength(len)), + "" => Ok(Self::LineOffset(len)), _ => Err(SvgdxError::InvalidData(format!( "Invalid LocSpec format {value}" ))), @@ -512,18 +512,24 @@ mod test { #[test] fn test_get_point() { let bb = BoundingBox::new(10., 10., 20., 20.); - assert_eq!(bb.locspec("t:2".parse().expect("test")), (12., 10.)); - assert_eq!(bb.locspec("r:25%".parse().expect("test")), (20., 12.5)); - assert_eq!(bb.locspec("b:6".parse().expect("test")), (16., 20.)); - assert_eq!(bb.locspec("l:150%".parse().expect("test")), (10., 25.)); - assert_eq!(bb.locspec("tl".parse().expect("test")), (10., 10.)); - assert_eq!(bb.locspec("t".parse().expect("test")), (15., 10.)); - assert_eq!(bb.locspec("tr".parse().expect("test")), (20., 10.)); - assert_eq!(bb.locspec("r".parse().expect("test")), (20., 15.)); - assert_eq!(bb.locspec("br".parse().expect("test")), (20., 20.)); - assert_eq!(bb.locspec("b".parse().expect("test")), (15., 20.)); - assert_eq!(bb.locspec("bl".parse().expect("test")), (10., 20.)); - assert_eq!(bb.locspec("l".parse().expect("test")), (10., 15.)); - assert_eq!(bb.locspec("c".parse().expect("test")), (15., 15.)); + assert_eq!(bb.locspec("t:2".parse().expect("test")), Some((12., 10.))); + assert_eq!( + bb.locspec("r:25%".parse().expect("test")), + Some((20., 12.5)) + ); + assert_eq!(bb.locspec("b:6".parse().expect("test")), Some((16., 20.))); + assert_eq!( + bb.locspec("l:150%".parse().expect("test")), + Some((10., 25.)) + ); + assert_eq!(bb.locspec("tl".parse().expect("test")), Some((10., 10.))); + assert_eq!(bb.locspec("t".parse().expect("test")), Some((15., 10.))); + assert_eq!(bb.locspec("tr".parse().expect("test")), Some((20., 10.))); + assert_eq!(bb.locspec("r".parse().expect("test")), Some((20., 15.))); + assert_eq!(bb.locspec("br".parse().expect("test")), Some((20., 20.))); + assert_eq!(bb.locspec("b".parse().expect("test")), Some((15., 20.))); + assert_eq!(bb.locspec("bl".parse().expect("test")), Some((10., 20.))); + assert_eq!(bb.locspec("l".parse().expect("test")), Some((10., 15.))); + assert_eq!(bb.locspec("c".parse().expect("test")), Some((15., 15.))); } } diff --git a/src/transform.rs b/src/transform.rs index 3d6ba67..70bd315 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -246,7 +246,9 @@ impl Transformer { } if !orig_svg_attrs.contains_key("viewBox") { - let (x1, y1) = bb.locspec(LocSpec::TopLeft); + let (x1, y1) = bb + .locspec(LocSpec::TopLeft) + .expect("using non lineoffset garenteed not to be none"); new_svg_attrs.insert( "viewBox", format!("{} {} {} {}", fstr(x1), fstr(y1), view_width, view_height).as_str(), From 2e788575a5603812e9540a65a8a29b78f565e21e Mon Sep 17 00:00:00 2001 From: megamaths Date: Mon, 18 Aug 2025 12:49:00 +0100 Subject: [PATCH 6/8] moved the corner radius to seperate branch --- src/elements/connector.rs | 77 +------------------------------------ src/elements/line_offset.rs | 38 +++++++++--------- 2 files changed, 20 insertions(+), 95 deletions(-) diff --git a/src/elements/connector.rs b/src/elements/connector.rs index 6546970..74b7b4d 100644 --- a/src/elements/connector.rs +++ b/src/elements/connector.rs @@ -79,7 +79,6 @@ pub struct Connector { end: Endpoint, conn_type: ConnectionType, offset: Option, - corner_radius: f32, } fn closest_loc( @@ -272,13 +271,6 @@ impl Connector { None }; - let corner_radius = if let Some(rad) = element.pop_attr("corner-radius") { - rad.parse() - .map_err(|_| SvgdxError::ParseError("Invalid corner-radius".to_owned()))? - } else { - 0.0 - }; - // This could probably be tidier, trying to deal with lots of combinations. // Needs to support explicit coordinate pairs or element references, and // for element references support given locations or not (in which case @@ -374,7 +366,6 @@ impl Connector { end_el: end_el.cloned(), conn_type, offset, - corner_radius, }) } @@ -926,16 +917,7 @@ impl Connector { )?; // TODO: remove repeated points. - if self.corner_radius != 0.0 { - SvgElement::new( - "path", - &[( - "d".to_string(), - Self::points_to_path(points, self.corner_radius), - )], - ) - .with_attrs_from(&self.source_element) - } else if points.len() == 2 { + if points.len() == 2 { SvgElement::new( "line", &[ @@ -964,61 +946,4 @@ impl Connector { }; Ok(conn_element) } - - fn points_to_path(points: Vec<(f32, f32)>, max_radius: f32) -> String { - let mut result = vec![]; - let mut radii = vec![]; - for i in 1..(points.len() - 1) { - let mut d1 = - (points[i].0 - points[i - 1].0).abs() + (points[i].1 - points[i - 1].1).abs(); - let mut d2 = - (points[i + 1].0 - points[i].0).abs() + (points[i + 1].1 - points[i].1).abs(); - if i != 1 { - d1 /= 2.0; - } - if i != points.len() - 2 { - d2 /= 2.0; - } - let radius = d1.min(d2).min(max_radius); - radii.push(radius); - } - - let mut pos = points[0]; - result.push(format!("M {} {}", pos.0, pos.1)); - - for i in 1..(points.len() - 1) { - let dx1 = points[i].0 - pos.0; - let dy1 = points[i].1 - pos.1; - let dx2 = points[i + 1].0 - points[i].0; - let dy2 = points[i + 1].1 - points[i].1; - - pos.0 += dx1 - dx1 * radii[i - 1] / (dx1 * dx1 + dy1 * dy1).sqrt(); - pos.1 += dy1 - dy1 * radii[i - 1] / (dx1 * dx1 + dy1 * dy1).sqrt(); - - result.push(format!("L {} {}", pos.0, pos.1)); - - let mut new_pos = points[i]; - - new_pos.0 += dx2 * radii[i - 1] / (dx2 * dx2 + dy2 * dy2).sqrt(); - new_pos.1 += dy2 * radii[i - 1] / (dx2 * dx2 + dy2 * dy2).sqrt(); - - let cl = (dx1 * dy2 - dy1 * dx2) > 0.0; - let cl_str = if cl { "1" } else { "0" }; - - result.push(format!( - "a {} {} 0 0 {} {} {}", - radii[i - 1], - radii[i - 1], - cl_str, - (new_pos.0 - pos.0), - (new_pos.1 - pos.1) - )); - - pos = new_pos; - } - pos = points[points.len() - 1]; - result.push(format!("L {} {}", pos.0, pos.1)); - - result.as_slice().join(" ") - } } diff --git a/src/elements/line_offset.rs b/src/elements/line_offset.rs index 3cd7d00..8653120 100644 --- a/src/elements/line_offset.rs +++ b/src/elements/line_offset.rs @@ -50,7 +50,7 @@ fn get_point_along_polyline(el: &SvgElement, length: Length) -> Result<(f32, f32 // loop to allow repeat to find total length if a percentage loop { let mut points = replaced_commas.split_whitespace(); - let mut cummulative_dist = 0.0; + let mut cumulative_dist = 0.0; lastx = 0.0; lasty = 0.0; let mut first_point = true; @@ -60,14 +60,14 @@ fn get_point_along_polyline(el: &SvgElement, length: Length) -> Result<(f32, f32 if !first_point { let len = ((lastx - x) * (lastx - x) + (lasty - y) * (lasty - y)).sqrt(); - if !is_percent && cummulative_dist + len > dist { - let ratio = (dist - cummulative_dist) / len; + 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, )); } - cummulative_dist += len; + cumulative_dist += len; } else if dist < 0.0 { // clamp to start return Ok((x, y)); @@ -81,7 +81,7 @@ fn get_point_along_polyline(el: &SvgElement, length: Length) -> Result<(f32, f32 break; } else { is_percent = false; - dist *= cummulative_dist; + dist *= cumulative_dist; } } return Ok((lastx, lasty)); @@ -105,7 +105,7 @@ fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { let mut pos; // loop to allow repeat to find total length if a percentage loop { - let mut cummulative_dist = 0.0; + let mut cumulative_dist = 0.0; pos = (0.0, 0.0); let mut last_stable_pos = pos; let mut r = 0.0; @@ -162,12 +162,12 @@ fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { pos.0 = val; } let d = (pos.0 - last_stable_pos.0).abs(); - if !is_percent && cummulative_dist + d > dist { - let r = (dist - cummulative_dist) / d; + 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, pos.1)); } - cummulative_dist += d; + cumulative_dist += d; last_stable_pos = pos; } else { return Err(SvgdxError::ParseError( @@ -183,12 +183,12 @@ fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { pos.1 = val; } let d = (pos.1 - last_stable_pos.1).abs(); - if !is_percent && cummulative_dist + d > dist { - let r = (dist - cummulative_dist) / d; + if !is_percent && cumulative_dist + d > dist { + let r = (dist - cumulative_dist) / d; return Ok((pos.0, last_stable_pos.1 * (1.0 - r) + pos.1 * r)); } - cummulative_dist += d; + cumulative_dist += d; last_stable_pos = pos; } else { return Err(SvgdxError::ParseError( @@ -213,15 +213,15 @@ fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { let d = ((last_stable_pos.0 - pos.0) * (last_stable_pos.0 - pos.0) + (last_stable_pos.1 - pos.1) * (last_stable_pos.1 - pos.1)) .sqrt(); - if !is_percent && cummulative_dist + d > dist { - let r = (dist - cummulative_dist) / d; + 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, )); } - cummulative_dist += d; + cumulative_dist += d; last_stable_pos = pos; } else { return Err(SvgdxError::ParseError( @@ -310,8 +310,8 @@ fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { }; let arc_length = arc_angle.abs() * r; - if !is_percent && cummulative_dist + arc_length > dist { - let ratio = (dist - cummulative_dist) / arc_length; + 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(( @@ -320,7 +320,7 @@ fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { )); } - cummulative_dist += arc_length; + cumulative_dist += arc_length; last_stable_pos = pos; } else { return Err(SvgdxError::ParseError( @@ -336,7 +336,7 @@ fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { break; } else { is_percent = false; - dist *= cummulative_dist; + dist *= cumulative_dist; } } return Ok(pos); From ce768e89ed636cb4ede9036f992bb8263ce01767 Mon Sep 17 00:00:00 2001 From: megamaths Date: Tue, 19 Aug 2025 13:48:40 +0100 Subject: [PATCH 7/8] improved readability and cohesion with rest of program (same error types) --- src/elements/line_offset.rs | 305 +++++++++++++++--------------------- 1 file changed, 130 insertions(+), 175 deletions(-) diff --git a/src/elements/line_offset.rs b/src/elements/line_offset.rs index 8653120..fee7bd3 100644 --- a/src/elements/line_offset.rs +++ b/src/elements/line_offset.rs @@ -1,6 +1,7 @@ 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 { @@ -14,10 +15,10 @@ fn get_point_along_line(el: &SvgElement, length: Length) -> Result<(f32, f32)> { el.get_attr("x2"), el.get_attr("y2"), ) { - let x1: f32 = x1.parse()?; - let y1: f32 = y1.parse()?; - let x2: f32 = x2.parse()?; - let y2: f32 = y2.parse()?; + 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)); } @@ -25,7 +26,7 @@ fn get_point_along_line(el: &SvgElement, length: Length) -> Result<(f32, f32)> { let ratio = if is_percent { dist } else { - let len = ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)).sqrt(); + let len = (x1 - x2).hypot(y1 - y2); dist / len }; return Ok((x1 + ratio * (x2 - x1), y1 + ratio * (y2 - y1))); @@ -37,52 +38,43 @@ fn get_point_along_line(el: &SvgElement, length: Length) -> Result<(f32, f32)> { } fn get_point_along_polyline(el: &SvgElement, length: Length) -> Result<(f32, f32)> { - let (mut is_percent, mut dist) = match length { + 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 replaced_commas = points.replace([','], " "); - let mut lastx; - let mut lasty; - - // loop to allow repeat to find total length if a percentage - loop { - let mut points = replaced_commas.split_whitespace(); - let mut cumulative_dist = 0.0; - lastx = 0.0; - lasty = 0.0; - let mut first_point = true; - while let (Some(x), Some(y)) = (points.next(), points.next()) { - let x: f32 = x.parse()?; - let y: f32 = y.parse()?; - - if !first_point { - let len = ((lastx - x) * (lastx - x) + (lasty - y) * (lasty - y)).sqrt(); - 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)); + 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, + )); } - lastx = x; - lasty = y; - - first_point = false; - } - if !is_percent { - break; - } else { - is_percent = false; - dist *= cumulative_dist; + 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)); } @@ -93,126 +85,99 @@ fn get_point_along_polyline(el: &SvgElement, length: Length) -> Result<(f32, f32 } fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { - let (mut is_percent, mut dist) = match length { + 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 replaced_commas = d.replace([','], " "); - let items = replaced_commas.split_whitespace(); - - let mut pos; - // loop to allow repeat to find total length if a percentage - loop { - let mut cumulative_dist = 0.0; - pos = (0.0, 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 items.clone() { - 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); - } + 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 { - if ['b', 'B', 'c', 'C', 'q', 'Q', 's', 'S', 't', 'T', 'z', 'Z'].contains(&op) { + } + } 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}" ))); - } else if op == 'm' || op == 'M' { - if arg_num == 0 { - let val = item.parse::()?; - if op == 'm' { - pos.0 += val; - } else { - pos.0 = val; - } - } else if arg_num == 1 { - let val = item.parse::()?; - if op == 'm' { - pos.1 += val; - } else { - pos.1 = val; - } - last_stable_pos = pos; - } else { - return Err(SvgdxError::ParseError( - "path has too many vars".to_string(), - )); - } - } else if op == 'h' || op == 'H' { - if arg_num == 0 { - let val = item.parse::()?; - if op == 'h' { - pos.0 += val; - } else { - pos.0 = val; - } - let d = (pos.0 - last_stable_pos.0).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, pos.1)); - } + } + }; + if arg_num == num_args { + return Err(SvgdxError::ParseError("path has too many vars".to_string())); + } - cumulative_dist += d; - last_stable_pos = pos; + 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 { - return Err(SvgdxError::ParseError( - "path has too many vars".to_string(), - )); + *pos_ref = val; } - } else if op == 'v' || op == 'V' { - if arg_num == 0 { - let val = item.parse::()?; - if op == 'v' { - pos.1 += val; - } else { - pos.1 = val; - } - let d = (pos.1 - last_stable_pos.1).abs(); - if !is_percent && cumulative_dist + d > dist { - let r = (dist - cumulative_dist) / d; - return Ok((pos.0, last_stable_pos.1 * (1.0 - r) + pos.1 * r)); - } - - cumulative_dist += d; - last_stable_pos = pos; + } + 'h' | 'H' | 'v' | 'V' => { + let val = strp(&item)?; + let pos_ref = if op == 'h' || op == 'H' { + &mut pos.0 } else { - return Err(SvgdxError::ParseError( - "path has too many vars".to_string(), + &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, )); } - } else if op == 'l' || op == 'L' { + + cumulative_dist += d; + } + 'l' | 'L' => { + let val = strp(&item)?; if arg_num == 0 { - let val = item.parse::()?; if op == 'l' { pos.0 += val; } else { pos.0 = val; } - } else if arg_num == 1 { - let val = item.parse::()?; + } else { if op == 'l' { pos.1 += val; } else { pos.1 = val; } - let d = ((last_stable_pos.0 - pos.0) * (last_stable_pos.0 - pos.0) - + (last_stable_pos.1 - pos.1) * (last_stable_pos.1 - pos.1)) - .sqrt(); + 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(( @@ -222,18 +187,14 @@ fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { } cumulative_dist += d; - last_stable_pos = pos; - } else { - return Err(SvgdxError::ParseError( - "path has too many vars".to_string(), - )); } - } else if op == 'a' || op == 'A' { + } + 'a' | 'A' => { if arg_num == 0 { - let val = item.parse::()?; + let val = strp(&item)?; r = val; } else if arg_num == 1 { - let val = item.parse::()?; + let val = strp(&item)?; if r != val { return Err(SvgdxError::ParseError( "path length not supported for non circle elipse".to_string(), @@ -248,14 +209,14 @@ fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { let val = item.parse::()?; sweeping_flag = val != 0; } else if arg_num == 5 { - let val = item.parse::()?; + let val = strp(&item)?; if op == 'a' { pos.0 += val; } else { pos.0 = val; } - } else if arg_num == 6 { - let val = item.parse::()?; + } else { + let val = strp(&item)?; if op == 'a' { pos.1 += val; } else { @@ -321,23 +282,24 @@ fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { } cumulative_dist += arc_length; - last_stable_pos = pos; - } else { - return Err(SvgdxError::ParseError( - "path has too many vars".to_string(), - )); } } + _ => { + return Err(SvgdxError::InvalidData(format!( + "not yet impl path parsing line offset for {op}" + ))); + } + } + + arg_num += 1; - arg_num += 1; + if arg_num == num_args { + last_stable_pos = pos; } } - if !is_percent { - break; - } else { - is_percent = false; - dist *= cumulative_dist; - } + } + if is_percent { + return get_point_along_path(el, Length::Absolute(dist * cumulative_dist)); } return Ok(pos); } @@ -346,21 +308,14 @@ fn get_point_along_path(el: &SvgElement, length: Length) -> Result<(f32, f32)> { } pub fn get_point_along_linelike_type_el(el: &SvgElement, length: Length) -> Result<(f32, f32)> { - let name = el.name(); - - if name == "line" { - return get_point_along_line(el, length); - } - if name == "polyline" { - return get_point_along_polyline(el, length); + 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(), + )), } - if name == "path" { - return get_point_along_path(el, length); - } - - Err(SvgdxError::MissingAttribute( - "looking for point on line in a non line element".to_string(), - )) } #[cfg(test)] From 2bdac6c05ed0606652b3d3f82760103dbc6e77b1 Mon Sep 17 00:00:00 2001 From: megamaths Date: Mon, 22 Sep 2025 22:17:38 +0100 Subject: [PATCH 8/8] moved lineoffset to new enum ElementLoc and moved render corner match to another file --- src/elements/connector.rs | 617 ++++----------------------- src/elements/corner_route.rs | 438 +++++++++++++++++++ src/elements/layout.rs | 57 +-- src/elements/mod.rs | 1 + src/elements/text.rs | 17 +- src/geometry/bbox.rs | 29 +- src/geometry/mod.rs | 4 +- src/geometry/types.rs | 77 ++-- src/transform.rs | 4 +- tests/integration_tests/connector.rs | 15 + 10 files changed, 632 insertions(+), 627 deletions(-) create mode 100644 src/elements/corner_route.rs diff --git a/src/elements/connector.rs b/src/elements/connector.rs index 74b7b4d..11c57dc 100644 --- a/src/elements/connector.rs +++ b/src/elements/connector.rs @@ -1,11 +1,10 @@ -use std::cmp::Ordering; -use std::collections::BinaryHeap; - use super::SvgElement; use crate::context::ElementMap; -use crate::elements::line_offset::get_point_along_linelike_type_el; +use crate::elements::corner_route::render_match_corner; use crate::errors::{Result, SvgdxError}; -use crate::geometry::{parse_el_loc, strp_length, BoundingBox, 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 { @@ -13,7 +12,7 @@ pub fn is_connector(el: &SvgElement) -> bool { } #[derive(Clone, Copy, Debug, PartialEq)] -enum Direction { +pub enum Direction { Up, Right, Down, @@ -21,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 { @@ -75,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, } @@ -95,9 +94,7 @@ fn closest_loc( .ok_or_else(|| SvgdxError::MissingBoundingBox(this.to_string()))?; for loc in edge_locations(conn_type) { - let this_coord = this_bb - .locspec(loc) - .expect("always some as LineOffset not in edge_locations"); + let this_coord = this_bb.locspec(loc); let ((x1, y1), (x2, y2)) = (this_coord, point); let dist_sq = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); if dist_sq < min_dist_sq { @@ -127,12 +124,8 @@ fn shortest_link( for this_loc in edge_locations(conn_type) { for that_loc in edge_locations(conn_type) { - let this_coord = this_bb - .locspec(this_loc) - .expect("always some as LineOffset not in edge_locations"); - let that_coord = that_bb - .locspec(that_loc) - .expect("always some as LineOffset not in edge_locations"); + 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); @@ -146,35 +139,10 @@ fn shortest_link( Ok((this_min_loc, that_min_loc)) } -// from the exaple 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)) - } -} -struct ElementParseData<'a> { - el: Option<&'a SvgElement>, - loc: Option, - point: Option<(f32, f32)>, - dir: Option, +#[allow(clippy::large_enum_variant)] +enum ElementParseData { + El(SvgElement, Option, Option), + Point(f32, f32), } impl Connector { @@ -188,32 +156,37 @@ impl Connector { } } - fn parse_element<'a>( + fn parse_element( element: &mut SvgElement, - elem_map: &'a impl ElementMap, + elem_map: &impl ElementMap, attr_name: &str, - ) -> Result> { + ) -> Result { let this_ref = element .pop_attr(attr_name) .ok_or_else(|| SvgdxError::MissingAttribute(attr_name.to_string()))?; - let mut ret = ElementParseData { - el: None, - loc: None, - point: None, - dir: None, - }; - // 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 { - ret.dir = Self::loc_to_dir(loc); - ret.loc = Some(loc); + if let ElementLoc::LocSpec(ls) = loc { + retdir = Self::loc_to_dir(ls); + } + retloc = Some(loc); } - ret.el = elem_map.get_element(&elref); + 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()); - ret.point = Some(( + + Ok(ElementParseData::Point( parts.next().ok_or_else(|| { SvgdxError::InvalidData( (attr_name.to_owned() + "_ref x should be numeric").to_owned(), @@ -224,28 +197,8 @@ impl Connector { (attr_name.to_owned() + "_ref y should be numeric").to_owned(), ) })?, - )); + )) } - - Ok(ret) - } - - fn get_coord_element_loc( - elem_map: &impl ElementMap, - el: &SvgElement, - loc: LocSpec, - ) -> Result<(f32, f32)> { - if let LocSpec::LineOffset(l) = loc { - return get_point_along_linelike_type_el(el, l); - } - - let coord = elem_map - .get_element_bbox(el)? - .ok_or_else(|| SvgdxError::MissingBoundingBox(el.to_string()))? - .locspec(loc) - .expect("only None if LineOffset and know is not"); - - Ok(coord) } pub fn from_element( @@ -257,10 +210,14 @@ impl Connector { 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, start_point, mut start_dir) = - (start_ret.el, start_ret.loc, start_ret.point, start_ret.dir); - let (end_el, mut end_loc, end_point, mut end_dir) = - (end_ret.el, end_ret.loc, end_ret.point, end_ret.dir); + 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( @@ -282,18 +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 = Self::get_coord_element_loc( - elem_map, - end_el, - 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), @@ -301,17 +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 = Self::get_coord_element_loc( - elem_map, - start_el, - 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), @@ -320,38 +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 = Self::get_coord_element_loc( - elem_map, - end_el, - 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 = Self::get_coord_element_loc( - elem_map, - start_el, - 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 = - Self::get_coord_element_loc(elem_map, start_el, start_loc.expect("Set above"))?; + start_el.get_element_loc_coord(elem_map, start_loc.expect("Set above"))?; let end_coord = - Self::get_coord_element_loc(elem_map, end_el, end_loc.expect("Set above"))?; + 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), @@ -362,435 +312,13 @@ impl Connector { source_element: element, start, end, - start_el: start_el.cloned(), - end_el: end_el.cloned(), + start_el, + end_el, conn_type, offset, }) } - // 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 render_match_corner_get_lines( - &self, - ratio_offset: f32, - abs_offsets: (f32, f32), - bbs: (BoundingBox, BoundingBox), - abs_offset_set: bool, - dirs: (Direction, Direction), - ) -> (Vec, Vec, usize, usize) { - let (x1, y1) = self.start.origin; - let (x2, y2) = self.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 render_match_corner_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 - && !Self::aals_blocked_by_bb( - start_el_bb, - point_set[i].1, - point_set[j].1, - false, - point_set[i].0, - ) - && !Self::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 - && !Self::aals_blocked_by_bb( - start_el_bb, - point_set[i].0, - point_set[j].0, - true, - point_set[i].1, - ) - && !Self::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 render_match_corner_add_start_and_end( - &self, - 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) = self.start.origin; - let (x2, y2) = self.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)) - && !Self::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)) - && !Self::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)) - && !Self::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)) - && !Self::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 render_match_corner_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 = 1000 + 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 render_match_corner_dijkstra_get_dists( - point_set: &[(f32, f32)], - edge_set: &[Vec], - edge_costs: &[Vec], - start_ind: usize, - end_ind: usize, - total_bb_size: u32, - ) -> Vec { - // just needs to be bigger than 5* (corner cost + total bounding box size) - let inf = 1000000 + 10 * total_bb_size; - 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 render_match_corner_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 - } - - fn render_match_corner( - &self, - 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) = self.start.origin; - let (x2, y2) = self.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)) = (self.start.dir, self.end.dir) { - // x_lines have constant x vary over y - let (x_lines, y_lines, mid_x, mid_y) = self.render_match_corner_get_lines( - 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 = - Self::render_match_corner_get_edges(&point_set, start_el_bb, end_el_bb); - let (start_ind, end_ind) = self.render_match_corner_add_start_and_end( - &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()) as u32; - - let edge_costs = Self::render_match_corner_cost_function( - &point_set, - &edge_set, - x_lines, - y_lines, - mid_x, - mid_y, - total_bb_size, - ); - - let dist = Self::render_match_corner_dijkstra_get_dists( - &point_set, - &edge_set, - &edge_costs, - start_ind, - end_ind, - total_bb_size, - ); - - points = Self::render_match_corner_dijkstra_get_points( - dist, - &point_set, - &edge_set, - &edge_costs, - start_ind, - end_ind, - ); - } else { - points = vec![(x1, y1), (x2, y2)]; - } - - Ok(points) - } - pub fn render(&self, ctx: &impl ElementMap) -> Result { let default_ratio_offset = Length::Ratio(0.5); let default_abs_offset = Length::Absolute(3.); @@ -907,7 +435,8 @@ impl Connector { end_el_bb = el_bb; } } - let points = self.render_match_corner( + let points = render_match_corner( + self, ratio_offset, start_abs_offset, end_abs_offset, 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 450af1a..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}; @@ -78,11 +79,11 @@ fn expand_single_relspec(value: &str, ctx: &impl ElementMap) -> String { }; if let Ok((Some(elem), rest)) = split_relspec(value, ctx) { if rest.is_empty() && elem.name() == "point" { - if let Ok(Some(Some(point))) = elem_loc(elem, LocSpec::Center) { + if let Ok(Some(point)) = elem_loc(elem, LocSpec::Center) { return format!("{} {}", fstr(point.0), fstr(point.1)); } } else if let Some(loc) = rest.strip_prefix(LOCSPEC_SEP).and_then(|s| s.parse().ok()) { - if let Ok(Some(Some(point))) = elem_loc(elem, loc) { + if let Ok(Some(point)) = elem_loc(elem, loc) { return format!("{} {}", fstr(point.0), fstr(point.1)); } } else if let Some(scalar) = rest @@ -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")); @@ -645,9 +660,7 @@ impl SvgElement { } else { 0. }; - let (x, y) = bbox - .locspec(rel.to_locspec()) - .expect("rel is not lineoffset and using non lineoffset garenteed not to be none"); + let (x, y) = bbox.locspec(rel.to_locspec()); let (dx, dy) = match rel { DirSpec::Above => (-this_width / 2., -(this_height + gap)), DirSpec::Below => (-this_width / 2., gap), @@ -660,9 +673,7 @@ impl SvgElement { // Need to determine top-left corner of the target bbox which // may not be (0, 0), and offset by the equivalent amount. if let Some(bbox) = ctx.get_target_element(self)?.bbox()? { - let (tx, ty) = bbox - .locspec(LocSpec::TopLeft) - .expect("using non lineoffset garenteed not to be none"); + let (tx, ty) = bbox.locspec(LocSpec::TopLeft); pos.xmin = Some(x + dx - tx); pos.ymin = Some(y + dy - ty); } @@ -683,9 +694,7 @@ fn position_from_bbox(element: &mut SvgElement, bb: &BoundingBox, inscribe: bool let width = bb.width(); let height = bb.height(); let (cx, cy) = bb.center(); - let (x1, y1) = bb - .locspec(LocSpec::TopLeft) - .expect("using non lineoffset garenteed not to be none"); + let (x1, y1) = bb.locspec(LocSpec::TopLeft); match element.name() { "rect" | "box" => { element.set_attr("x", &fstr(x1)); @@ -830,19 +839,14 @@ fn pos_attr_helper( "Could not parse '{loc_str}' in this context", ))); } - if let Some((x, y)) = bbox.locspec(loc) { - let (dx, dy) = extract_dx_dy(dxy)?; - use ScalarSpec::*; - v = match attr_ss { - Minx | Maxx | Cx => x + dx, - Miny | Maxy | Cy => y + dy, - _ => v, - }; - } else { - return Err(SvgdxError::InvalidData( - "general use of lineoffset not yet supported".to_string(), - )); - } + let (x, y) = bbox.locspec(loc); + let (dx, dy) = extract_dx_dy(dxy)?; + use ScalarSpec::*; + v = match attr_ss { + Minx | Maxx | Cx => x + dx, + Miny | Maxy | Cy => y + dy, + _ => v, + }; } Ok(fstr(v).to_string()) } @@ -904,7 +908,6 @@ fn eval_text_anchor(element: &mut SvgElement, ctx: &impl ContextView) -> Result< LocSpec::BottomEdge(_) => element.set_default_attr("text-loc", "b"), LocSpec::LeftEdge(_) => element.set_default_attr("text-loc", "l"), LocSpec::RightEdge(_) => element.set_default_attr("text-loc", "r"), - LocSpec::LineOffset(_) => element.set_default_attr("text-loc", "c"), } } else { return Err(SvgdxError::InvalidData(format!( diff --git a/src/elements/mod.rs b/src/elements/mod.rs index 52b1fdd..3d4beb3 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -1,6 +1,7 @@ mod bearing; mod connector; mod containers; +mod corner_route; mod element; mod layout; mod line_offset; diff --git a/src/elements/text.rs b/src/elements/text.rs index bb64789..9854321 100644 --- a/src/elements/text.rs +++ b/src/elements/text.rs @@ -140,19 +140,14 @@ fn get_text_position(element: &mut SvgElement) -> Result<(f32, f32, bool, LocSpe // Assumption is that text should be centered within the rect, // and has styling via CSS to reflect this, e.g.: // text.d-text { dominant-baseline: central; text-anchor: middle; } - if let Some((mut tdx, mut tdy)) = element + let (mut tdx, mut tdy) = element .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)) - } else { - Err(SvgdxError::InvalidData( - "trying to use lineoffset on text element not supported yet".to_string(), - )) - } + .locspec(text_anchor); + + tdx += t_dx; + tdy += t_dy; + Ok((tdx, tdy, outside, text_anchor, text_classes)) } pub fn process_text_attr(element: &SvgElement) -> Result<(SvgElement, Vec)> { diff --git a/src/geometry/bbox.rs b/src/geometry/bbox.rs index 1be77db..7e120d9 100644 --- a/src/geometry/bbox.rs +++ b/src/geometry/bbox.rs @@ -44,7 +44,7 @@ impl BoundingBox { Self { x1, y1, x2, y2 } } - pub fn locspec(&self, ls: LocSpec) -> Option<(f32, f32)> { + pub fn locspec(&self, ls: LocSpec) -> (f32, f32) { let tl = (self.x1, self.y1); let tr = (self.x2, self.y1); let br = (self.x2, self.y2); @@ -52,20 +52,19 @@ impl BoundingBox { let c = ((self.x1 + self.x2) / 2., (self.y1 + self.y2) / 2.); use LocSpec::*; match ls { - TopLeft => Some(tl), - Top => Some(((self.x1 + self.x2) / 2., self.y1)), - TopRight => Some(tr), - Right => Some((self.x2, (self.y1 + self.y2) / 2.)), - BottomRight => Some(br), - Bottom => Some(((self.x1 + self.x2) / 2., self.y2)), - BottomLeft => Some(bl), - Left => Some((self.x1, (self.y1 + self.y2) / 2.)), - Center => Some(c), - TopEdge(len) => Some((len.calc_offset(self.x1, self.x2), self.y1)), - RightEdge(len) => Some((self.x2, len.calc_offset(self.y1, self.y2))), - BottomEdge(len) => Some((len.calc_offset(self.x1, self.x2), self.y2)), - LeftEdge(len) => Some((self.x1, len.calc_offset(self.y1, self.y2))), - LineOffset(_) => None, + TopLeft => tl, + Top => ((self.x1 + self.x2) / 2., self.y1), + TopRight => tr, + Right => (self.x2, (self.y1 + self.y2) / 2.), + BottomRight => br, + Bottom => ((self.x1 + self.x2) / 2., self.y2), + BottomLeft => bl, + Left => (self.x1, (self.y1 + self.y2) / 2.), + Center => c, + TopEdge(len) => (len.calc_offset(self.x1, self.x2), self.y1), + RightEdge(len) => (self.x2, len.calc_offset(self.y1, self.y2)), + BottomEdge(len) => (len.calc_offset(self.x1, self.x2), self.y2), + LeftEdge(len) => (self.x1, len.calc_offset(self.y1, self.y2)), } } 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 82782c6..b3fd960 100644 --- a/src/geometry/types.rs +++ b/src/geometry/types.rs @@ -139,6 +139,12 @@ pub enum LocSpec { RightEdge(Length), BottomEdge(Length), LeftEdge(Length), +} + +/// `LocSpec` defines a location on a `SvgElement` +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ElementLoc { + LocSpec(LocSpec), LineOffset(Length), } @@ -172,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; @@ -194,7 +222,6 @@ impl FromStr for LocSpec { "r" => Ok(Self::RightEdge(len)), "b" => Ok(Self::BottomEdge(len)), "l" => Ok(Self::LeftEdge(len)), - "" => Ok(Self::LineOffset(len)), _ => Err(SvgdxError::InvalidData(format!( "Invalid LocSpec format {value}" ))), @@ -342,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)); @@ -394,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!( @@ -512,24 +545,18 @@ mod test { #[test] fn test_get_point() { let bb = BoundingBox::new(10., 10., 20., 20.); - assert_eq!(bb.locspec("t:2".parse().expect("test")), Some((12., 10.))); - assert_eq!( - bb.locspec("r:25%".parse().expect("test")), - Some((20., 12.5)) - ); - assert_eq!(bb.locspec("b:6".parse().expect("test")), Some((16., 20.))); - assert_eq!( - bb.locspec("l:150%".parse().expect("test")), - Some((10., 25.)) - ); - assert_eq!(bb.locspec("tl".parse().expect("test")), Some((10., 10.))); - assert_eq!(bb.locspec("t".parse().expect("test")), Some((15., 10.))); - assert_eq!(bb.locspec("tr".parse().expect("test")), Some((20., 10.))); - assert_eq!(bb.locspec("r".parse().expect("test")), Some((20., 15.))); - assert_eq!(bb.locspec("br".parse().expect("test")), Some((20., 20.))); - assert_eq!(bb.locspec("b".parse().expect("test")), Some((15., 20.))); - assert_eq!(bb.locspec("bl".parse().expect("test")), Some((10., 20.))); - assert_eq!(bb.locspec("l".parse().expect("test")), Some((10., 15.))); - assert_eq!(bb.locspec("c".parse().expect("test")), Some((15., 15.))); + assert_eq!(bb.locspec("t:2".parse().expect("test")), (12., 10.)); + assert_eq!(bb.locspec("r:25%".parse().expect("test")), (20., 12.5)); + assert_eq!(bb.locspec("b:6".parse().expect("test")), (16., 20.)); + assert_eq!(bb.locspec("l:150%".parse().expect("test")), (10., 25.)); + assert_eq!(bb.locspec("tl".parse().expect("test")), (10., 10.)); + assert_eq!(bb.locspec("t".parse().expect("test")), (15., 10.)); + assert_eq!(bb.locspec("tr".parse().expect("test")), (20., 10.)); + assert_eq!(bb.locspec("r".parse().expect("test")), (20., 15.)); + assert_eq!(bb.locspec("br".parse().expect("test")), (20., 20.)); + assert_eq!(bb.locspec("b".parse().expect("test")), (15., 20.)); + assert_eq!(bb.locspec("bl".parse().expect("test")), (10., 20.)); + assert_eq!(bb.locspec("l".parse().expect("test")), (10., 15.)); + assert_eq!(bb.locspec("c".parse().expect("test")), (15., 15.)); } } diff --git a/src/transform.rs b/src/transform.rs index 70bd315..3d6ba67 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -246,9 +246,7 @@ impl Transformer { } if !orig_svg_attrs.contains_key("viewBox") { - let (x1, y1) = bb - .locspec(LocSpec::TopLeft) - .expect("using non lineoffset garenteed not to be none"); + let (x1, y1) = bb.locspec(LocSpec::TopLeft); new_svg_attrs.insert( "viewBox", format!("{} {} {} {}", fstr(x1), fstr(y1), view_width, view_height).as_str(), 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); +}