From dda3e16d2ea62cf0d2ed2459b2c473a7c0a7dfd8 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 13 Dec 2018 14:04:17 -0800 Subject: [PATCH] wip, too slow --- Cargo.lock | 50 +++++- utils/tile-svg/Cargo.toml | 3 +- utils/tile-svg/src/main.rs | 356 ++++++++++++++++++++++++++++++++++--- 3 files changed, 384 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 445ea467..dbb9af46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,6 +358,14 @@ dependencies = [ "termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "euclid" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "euclid" version = "0.19.0" @@ -749,6 +757,24 @@ dependencies = [ "linked-hash-map 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "lyon_algorithms" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lyon_path 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lyon_geom" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "euclid 0.18.2 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "lyon_geom" version = "0.12.1" @@ -759,6 +785,14 @@ dependencies = [ "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "lyon_path" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lyon_geom 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "lyon_path" version = "0.12.0" @@ -868,6 +902,14 @@ dependencies = [ "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "num-traits" version = "0.2.6" @@ -1452,7 +1494,8 @@ name = "tile-svg" version = "0.1.0" dependencies = [ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "euclid 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)", + "euclid 0.18.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lyon_algorithms 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", "quick-xml 0.12.4 (registry+https://github.com/rust-lang/crates.io-index)", "quickcheck 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1674,6 +1717,7 @@ source = "git+https://github.com/SergioBenitez/ring?branch=v0.12#9ccfa153a27aecc "checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" "checksum encoding_rs 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1a8fa54e6689eb2549c4efed8d00d7f3b2b994a064555b0e8df4ae3764bcc4be" "checksum env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)" = "15b0a4d2e39f8420210be8b27eeda28029729e2fd4291019455016c348240c38" +"checksum euclid 0.18.2 (registry+https://github.com/rust-lang/crates.io-index)" = "59b34ec7d95d70d5cda27301d6182bc17abce8b5b52e260f5ff32c677923bbb0" "checksum euclid 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)" = "70a2ebdf55fb9d6329046e026329a55ef8fbaae5ea833f56e170beb3125a8a5f" "checksum expat-sys 2.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c470ccb972f2088549b023db8029ed9da9426f5affbf9b62efff7009ab8ed5b1" "checksum failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7efb22686e4a466b1ec1a15c2898f91fa9cb340452496dca654032de20ff95b9" @@ -1715,7 +1759,10 @@ source = "git+https://github.com/SergioBenitez/ring?branch=v0.12#9ccfa153a27aecc "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fcce5fa49cc693c312001daf1d13411c4a5283796bac1084299ea3e567113f" "checksum lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d06ff7ff06f729ce5f4e227876cb88d10bc59cd4ae1e09fbb2bde15c850dc21" +"checksum lyon_algorithms 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b3ebe7b9bffa94e7d8d332992bd1d8b9c0318170862487f0a2d7e04bcc5aabbe" +"checksum lyon_geom 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c8ee0dc4aec93a8fd9109362bebfbad0ace69b8629937f954ecc8eea1de63146" "checksum lyon_geom 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d69cc8d0b54ed6d49ed2ef6b465e67ee89e92dfcb4bd839cbd58dc189c14efe8" +"checksum lyon_path 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "98c39b845796d4590197e2b2b97202e31b69071116a541bfddb52f50680318f0" "checksum lyon_path 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e9dc8e0746b7cca11960b602f7fe037bb067746a01eab4aa502fed1494544843" "checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" "checksum malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" @@ -1730,6 +1777,7 @@ source = "git+https://github.com/SergioBenitez/ring?branch=v0.12#9ccfa153a27aecc "checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" "checksum num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "af3fdbbc3291a5464dc57b03860ec37ca6bf915ed6ee385e7c6c052c422b2124" "checksum num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" +"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" "checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" "checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" "checksum objc 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9833ab0efe5361b1e2122a0544a5d3359576911a42cb098c2e59be8650807367" diff --git a/utils/tile-svg/Cargo.toml b/utils/tile-svg/Cargo.toml index 607d0cd0..a82d703d 100644 --- a/utils/tile-svg/Cargo.toml +++ b/utils/tile-svg/Cargo.toml @@ -6,7 +6,8 @@ edition = "2018" [dependencies] bitflags = "1.0" -euclid = "0.19" +euclid = "0.18" +lyon_algorithms = "0.11" quick-xml = "0.12" svgtypes = "0.2" diff --git a/utils/tile-svg/src/main.rs b/utils/tile-svg/src/main.rs index 07cf30dd..e7280f3a 100644 --- a/utils/tile-svg/src/main.rs +++ b/utils/tile-svg/src/main.rs @@ -16,14 +16,16 @@ extern crate quickcheck; #[cfg(test)] extern crate rand; -use euclid::{Point2D, Transform2D}; +use euclid::{Point2D, Rect, Transform2D, Vector2D}; +use lyon_algorithms::geom::{CubicBezierSegment, LineSegment, QuadraticBezierSegment}; use quick_xml::Reader; use quick_xml::events::Event; +use std::cmp::Ordering; use std::env; use std::mem; -use std::ops::Range; use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::time::Instant; use svgtypes::{Color as SvgColor, PathParser, PathSegment as SvgPathSegment, TransformListParser}; use svgtypes::{TransformListToken}; @@ -57,7 +59,13 @@ impl ComputedStyle { fn main() { let path = PathBuf::from(env::args().skip(1).next().unwrap()); let scene = Scene::from_path(&path); - println!("{:#?}", scene); + + let start_time = Instant::now(); + scene.generate_tiles(); + let elapsed_time = Instant::now() - start_time; + println!("{}ms elapsed", + elapsed_time.as_secs() as f64 * 1000.0 + + elapsed_time.subsec_micros() as f64 / 1000.0); } #[derive(Debug)] @@ -195,6 +203,14 @@ impl Scene { fn get_style(&self, style: StyleId) -> &ComputedStyle { &self.styles[style.0 as usize] } + + fn generate_tiles(&self) { + for object in &self.objects { + let mut tiler = Tiler::from_outline(&object.outline); + tiler.generate_tiles(); + // TODO(pcwalton) + } + } } impl PathObject { @@ -211,6 +227,7 @@ impl PathObject { #[derive(Debug)] struct Outline { contours: Vec, + bounds: Rect, } #[derive(Debug)] @@ -230,6 +247,7 @@ impl Outline { fn new() -> Outline { Outline { contours: vec![], + bounds: Rect::zero(), } } @@ -237,7 +255,9 @@ impl Outline { where I: Iterator { let mut outline = Outline::new(); let mut current_contour = Contour::new(); + let mut bounding_points = None; let (mut first_point_in_path, mut last_ctrl_point, mut last_point) = (None, None, None); + for segment in segments { match segment { SvgPathSegment::MoveTo { abs, x, y } => { @@ -250,7 +270,8 @@ impl Outline { last_ctrl_point = None; current_contour.push_transformed_point(&to, PointFlags::empty(), - &style.transform); + &style.transform, + &mut bounding_points); } SvgPathSegment::LineTo { abs, x, y } => { let to = compute_point(x, y, abs, &last_point); @@ -258,7 +279,8 @@ impl Outline { last_ctrl_point = None; current_contour.push_transformed_point(&to, PointFlags::empty(), - &style.transform); + &style.transform, + &mut bounding_points); } SvgPathSegment::HorizontalLineTo { abs, x } => { let to = Point2D::new(compute_point(x, 0.0, abs, &last_point).x, @@ -267,7 +289,8 @@ impl Outline { last_ctrl_point = None; current_contour.push_transformed_point(&to, PointFlags::empty(), - &style.transform); + &style.transform, + &mut bounding_points); } SvgPathSegment::VerticalLineTo { abs, y } => { let to = Point2D::new(last_point.unwrap_or(Point2D::zero()).x, @@ -276,7 +299,8 @@ impl Outline { last_ctrl_point = None; current_contour.push_transformed_point(&to, PointFlags::empty(), - &style.transform); + &style.transform, + &mut bounding_points); } SvgPathSegment::Quadratic { abs, x1, y1, x, y } => { let ctrl = compute_point(x1, y1, abs, &last_point); @@ -285,10 +309,12 @@ impl Outline { last_point = Some(to); current_contour.push_transformed_point(&ctrl, PointFlags::CONTROL_POINT_0, - &style.transform); + &style.transform, + &mut bounding_points); current_contour.push_transformed_point(&to, PointFlags::empty(), - &style.transform); + &style.transform, + &mut bounding_points); } SvgPathSegment::SmoothQuadratic { abs, x, y } => { let ctrl = last_point.unwrap_or(Point2D::zero()) + @@ -299,10 +325,12 @@ impl Outline { last_point = Some(to); current_contour.push_transformed_point(&ctrl, PointFlags::CONTROL_POINT_0, - &style.transform); + &style.transform, + &mut bounding_points); current_contour.push_transformed_point(&to, PointFlags::empty(), - &style.transform); + &style.transform, + &mut bounding_points); } SvgPathSegment::CurveTo { abs, x1, y1, x2, y2, x, y } => { let ctrl0 = compute_point(x1, y1, abs, &last_point); @@ -312,13 +340,16 @@ impl Outline { last_point = Some(to); current_contour.push_transformed_point(&ctrl0, PointFlags::CONTROL_POINT_0, - &style.transform); + &style.transform, + &mut bounding_points); current_contour.push_transformed_point(&ctrl1, PointFlags::CONTROL_POINT_1, - &style.transform); + &style.transform, + &mut bounding_points); current_contour.push_transformed_point(&to, PointFlags::empty(), - &style.transform); + &style.transform, + &mut bounding_points); } SvgPathSegment::SmoothCurveTo { abs, x2, y2, x, y } => { let ctrl0 = last_point.unwrap_or(Point2D::zero()) + @@ -330,13 +361,16 @@ impl Outline { last_point = Some(to); current_contour.push_transformed_point(&ctrl0, PointFlags::CONTROL_POINT_0, - &style.transform); + &style.transform, + &mut bounding_points); current_contour.push_transformed_point(&ctrl1, PointFlags::CONTROL_POINT_1, - &style.transform); + &style.transform, + &mut bounding_points); current_contour.push_transformed_point(&to, PointFlags::empty(), - &style.transform); + &style.transform, + &mut bounding_points); } SvgPathSegment::ClosePath { abs: _ } => { if !current_contour.is_empty() { @@ -351,6 +385,11 @@ impl Outline { if !current_contour.is_empty() { outline.contours.push(current_contour) } + + if let Some((upper_left, lower_right)) = bounding_points { + outline.bounds = Rect::from_points([upper_left, lower_right].into_iter()) + } + return outline; fn compute_point(x: f64, y: f64, abs: bool, last_point: &Option>) @@ -362,6 +401,10 @@ impl Outline { } } } + + fn segment_after(&self, endpoint_index: PointIndex) -> Segment { + self.contours[endpoint_index.contour_index].segment_after(endpoint_index.point_index) + } } impl Contour { @@ -379,24 +422,287 @@ impl Contour { fn push_transformed_point(&mut self, point: &Point2D, flags: PointFlags, - transform: &Transform2D) { - self.points.push(transform.transform_point(point)); + transform: &Transform2D, + bounding_points: &mut Option<(Point2D, Point2D)>) { + let point = transform.transform_point(point); + self.points.push(point); self.flags.push(flags); + + match *bounding_points { + Some((ref mut upper_left, ref mut lower_right)) => { + *upper_left = upper_left.min(point); + *lower_right = lower_right.max(point); + } + None => *bounding_points = Some((point, point)), + } } + + fn segment_after(&self, point_index: usize) -> Segment { + debug_assert!(self.point_is_endpoint(point_index)); + let point1_index = self.add_to_point_index(point_index, 1); + if self.point_is_endpoint(point1_index) { + return Segment::Line(LineSegment { + from: self.points[point_index], + to: self.points[point1_index], + }) + } + let point2_index = self.add_to_point_index(point_index, 2); + if self.point_is_endpoint(point2_index) { + return Segment::Quadratic(QuadraticBezierSegment { + from: self.points[point_index], + ctrl: self.points[point1_index], + to: self.points[point2_index], + }) + } + let point3_index = self.add_to_point_index(point_index, 3); + Segment::Cubic(CubicBezierSegment { + from: self.points[point_index], + ctrl1: self.points[point1_index], + ctrl2: self.points[point2_index], + to: self.points[point3_index], + }) + } + + fn point_is_endpoint(&self, point_index: usize) -> bool { + self.flags[point_index].intersects(PointFlags::CONTROL_POINT_0 | + PointFlags::CONTROL_POINT_1) + } + + fn add_to_point_index(&self, point_index: usize, addend: usize) -> usize { + (point_index + addend) % self.points.len() + } +} + +#[derive(Clone, Copy, Debug)] +struct PointIndex { + contour_index: usize, + point_index: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum Segment { + Line(LineSegment), + Quadratic(QuadraticBezierSegment), + Cubic(CubicBezierSegment), +} + +impl Segment { + fn endpoints(&self) -> (Point2D, Point2D) { + match *self { + Segment::Line(ref line) => (line.from, line.to), + Segment::Quadratic(ref curve) => (curve.from, curve.to), + Segment::Cubic(ref curve) => (curve.from, curve.to), + } + } + + // Note: If we convert these to monotonic then we can optimize this method. + // TODO(pcwalton): Consider changing the representation of `Segment` to remove the code + // duplication in the branches here? + fn min_y(&self) -> f32 { + match *self { + Segment::Line(ref line) => f32::min(line.from.y, line.to.y), + Segment::Quadratic(ref curve) => { + f32::min(f32::min(curve.from.y, curve.ctrl.y), curve.to.y) + } + Segment::Cubic(ref curve) => { + f32::min(f32::min(f32::min(curve.from.y, curve.ctrl1.y), curve.ctrl2.y), + curve.to.y) + } + } + } + + fn clip_y(&self, y: f32) -> ClippedSegments { + let (from, to) = self.endpoints(); + if from.y < y && to.y < y { + return ClippedSegments { min: Some(*self), max: None } + } + if from.y > y && to.y > y { + return ClippedSegments { min: None, max: Some(*self) } + } + + let (prev, next) = match *self { + Segment::Line(ref line) => { + let (prev, next) = line.split(line.solve_t_for_y(y)); + (Segment::Line(prev), Segment::Line(next)) + } + Segment::Quadratic(ref curve) => { + let (prev, next) = curve.split(curve.assume_monotonic().solve_t_for_y(y)); + (Segment::Quadratic(prev), Segment::Quadratic(next)) + } + Segment::Cubic(ref curve) => { + let swapped_curve = CubicBezierSegment { + from: curve.from.yx(), + ctrl1: curve.ctrl1.yx(), + ctrl2: curve.ctrl2.yx(), + to: curve.to.yx(), + }; + let (prev, next) = curve.split( + swapped_curve.assume_monotonic().solve_t_for_x(y, 0.0..1.0, TOLERANCE)); + (Segment::Cubic(prev), Segment::Cubic(next)) + } + }; + + if from.y <= to.y { + return ClippedSegments { min: Some(prev), max: Some(next) }; + } else { + return ClippedSegments { min: Some(next), max: Some(prev) }; + } + + const TOLERANCE: f32 = 0.01; + } + + fn translate(&self, by: &Vector2D) -> Segment { + match *self { + Segment::Line(ref line) => { + Segment::Line(LineSegment { + from: line.from + *by, + to: line.to + *by, + }) + } + Segment::Quadratic(ref curve) => { + Segment::Quadratic(QuadraticBezierSegment { + from: curve.from + *by, + ctrl: curve.ctrl + *by, + to: curve.to + *by, + }) + } + Segment::Cubic(ref curve) => { + Segment::Cubic(CubicBezierSegment { + from: curve.from + *by, + ctrl1: curve.ctrl1 + *by, + ctrl2: curve.ctrl2 + *by, + to: curve.to + *by, + }) + } + } + } +} + +struct ClippedSegments { + min: Option, + max: Option, } // Tiling -struct Tiler { - outline: Outline, +const TILE_WIDTH: f32 = 4.0; +const TILE_HEIGHT: f32 = 4.0; + +struct Tiler<'a> { + outline: &'a Outline, } -impl Tiler { - fn from_outline(outline: Outline) -> Tiler { +impl<'a> Tiler<'a> { + fn from_outline(outline: &Outline) -> Tiler { Tiler { outline, } } + + fn generate_tiles(&mut self) { + // Sort all edge indices. + let mut sorted_edge_indices = vec![]; + for contour_index in 0..self.outline.contours.len() { + let contour = &self.outline.contours[contour_index]; + for point_index in 0..contour.points.len() { + if contour.point_is_endpoint(point_index) { + sorted_edge_indices.push(PointIndex { contour_index, point_index }) + } + } + } + sorted_edge_indices.sort_by(|edge_index_a, edge_index_b| { + let segment_a = self.outline.segment_after(*edge_index_a); + let segment_b = self.outline.segment_after(*edge_index_b); + segment_a.min_y().partial_cmp(&segment_b.min_y()).unwrap_or(Ordering::Equal) + }); + + let bounds = self.outline.bounds; + let (max_x, max_y) = (bounds.max_x(), bounds.max_y()); + + let mut active_intervals = Intervals::new(max_x); + let mut active_edges = vec![]; + let mut next_edge_index_index = 0; + let mut tile_top = bounds.origin.y - bounds.origin.y % TILE_HEIGHT; + let mut strips = vec![]; + + while tile_top < max_y { + let mut strip = Strip::new(tile_top); + + // TODO(pcwalton): Populate tile strip with active intervals. + + for active_edge in mem::replace(&mut active_edges, vec![]) { + self.process_edge(active_edge, + &mut strip, + &mut active_edges, + &mut active_intervals); + } + + while next_edge_index_index < sorted_edge_indices.len() { + let segment = + self.outline.segment_after(sorted_edge_indices[next_edge_index_index]); + if segment.min_y() > strip.tile_bottom() { + break + } + + self.process_edge(segment, + &mut strip, + &mut active_edges, + &mut active_intervals); + next_edge_index_index += 1; + } + + tile_top = strip.tile_bottom(); + strips.push(strip); + } + } + + fn process_edge(&mut self, + edge: Segment, + strip: &mut Strip, + active_edges: &mut Vec, + intervals: &mut Intervals) { + let clipped = edge.clip_y(strip.tile_bottom()); + if let Some(upper_segment) = clipped.min { + strip.push_segment(upper_segment); + // FIXME(pcwalton): Assumes x-monotonicity! + // FIXME(pcwalton): The min call below is a hack! + let (from, to) = upper_segment.endpoints(); + let from_x = f32::max(0.0, f32::min(intervals.extent(), from.x)); + let to_x = f32::max(0.0, f32::min(intervals.extent(), to.x)); + if from_x < to_x { + intervals.add(IntervalRange::new(from_x, to_x, -1.0)) + } else { + intervals.add(IntervalRange::new(to_x, from_x, 1.0)) + } + } + if let Some(lower_segment) = clipped.max { + active_edges.push(lower_segment); + } + } +} + +// Strips + +struct Strip { + segments: Vec, + tile_top: f32, +} + +impl Strip { + fn new(tile_top: f32) -> Strip { + Strip { + segments: vec![], + tile_top, + } + } + + fn push_segment(&mut self, segment: Segment) { + self.segments.push(segment.translate(&Vector2D::new(0.0, -self.tile_top))) + } + + fn tile_bottom(&self) -> f32 { + self.tile_top + TILE_HEIGHT + } } // Intervals @@ -448,6 +754,10 @@ impl Intervals { self.ranges[0] = IntervalRange::new(0.0, end, 0.0); } + fn extent(&self) -> f32 { + self.ranges.last().unwrap().end + } + fn split_at(&mut self, value: f32) { let mut range_index = 0; while range_index < self.ranges.len() {