| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // The path division algorithm is mostly based on Raph Levien's curvature approximation[1] for |
| // quadratic Bézier division and Adrian Colomitchi's cubic-to-quadratic approximation[2] with a few |
| // additions to improve robustness. |
| // |
| // The algorithm converts all possible types of curves into primitives (lines and quadratic |
| // Béziers) sequentially, while these are pushed onto the `PathBuilder`. Afterwards, each `Path` |
| // is converted into lines in parallel. This part of the algorithms tries its best not to create |
| // new lines if two neighboring lines are close enough to forming a line together according to some |
| // threshold. |
| // |
| // [1]: https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html |
| // [2]: http://www.caffeineowl.com/graphics/2d/vectorial/bezierintro.html |
| |
| use std::{cell::RefCell, f32, rc::Rc}; |
| |
| use rayon::prelude::*; |
| |
| use crate::{ |
| extend::{ExtendTuple3, ExtendVec}, |
| lines::GeomId, |
| transform::GeomPresTransform, |
| Point, PIXEL_WIDTH, |
| }; |
| |
| // Pixel accuracy should be within 0.5 of a sub-pixel. |
| pub const MAX_ERROR: f32 = 1.0 / PIXEL_WIDTH as f32; |
| const MAX_ANGLE_ERROR: f32 = 0.001; |
| const MIN_LEN: usize = 256; |
| |
| fn lerp(t: f32, a: f32, b: f32) -> f32 { |
| t.mul_add(b, (-t).mul_add(a, a)) |
| } |
| |
| fn curvature(x: f32) -> f32 { |
| const C: f32 = 0.67; |
| x / (1.0 - C + ((x * x).mul_add(0.25, C * C * C * C)).sqrt().sqrt()) |
| } |
| |
| fn inv_curvature(k: f32) -> f32 { |
| const C: f32 = 0.39; |
| k * (1.0 - C + ((k * k).mul_add(0.25, C * C)).sqrt()) |
| } |
| |
| #[derive(Clone, Copy, Debug)] |
| pub struct WeightedPoint { |
| pub point: Point, |
| pub weight: f32, |
| } |
| |
| impl WeightedPoint { |
| pub fn applied(self) -> Point { |
| let w_recip = self.weight.recip(); |
| |
| Point { x: self.point.x * w_recip, y: self.point.y * w_recip } |
| } |
| } |
| |
| fn eval_cubic(t: f32, points: &[WeightedPoint; 4]) -> WeightedPoint { |
| let x = lerp( |
| t, |
| lerp( |
| t, |
| lerp(t, points[0].point.x, points[1].point.x), |
| lerp(t, points[1].point.x, points[2].point.x), |
| ), |
| lerp( |
| t, |
| lerp(t, points[1].point.x, points[2].point.x), |
| lerp(t, points[2].point.x, points[3].point.x), |
| ), |
| ); |
| let y = lerp( |
| t, |
| lerp( |
| t, |
| lerp(t, points[0].point.y, points[1].point.y), |
| lerp(t, points[1].point.y, points[2].point.y), |
| ), |
| lerp( |
| t, |
| lerp(t, points[1].point.y, points[2].point.y), |
| lerp(t, points[2].point.y, points[3].point.y), |
| ), |
| ); |
| let weight = lerp( |
| t, |
| lerp( |
| t, |
| lerp(t, points[0].weight, points[1].weight), |
| lerp(t, points[1].weight, points[2].weight), |
| ), |
| lerp( |
| t, |
| lerp(t, points[1].weight, points[2].weight), |
| lerp(t, points[2].weight, points[3].weight), |
| ), |
| ); |
| |
| WeightedPoint { point: Point { x, y }, weight } |
| } |
| |
| #[derive(Debug, Default)] |
| pub struct ScratchBuffers { |
| point_indices: Vec<usize>, |
| quad_indices: Vec<usize>, |
| point_commands: Vec<u32>, |
| } |
| |
| impl ScratchBuffers { |
| pub fn clear(&mut self) { |
| self.point_indices.clear(); |
| self.quad_indices.clear(); |
| self.point_commands.clear(); |
| } |
| } |
| |
| #[derive(Clone, Copy, Debug)] |
| enum PointCommand { |
| Start(usize), |
| Incr(f32), |
| End(usize, bool), |
| } |
| |
| impl From<u32> for PointCommand { |
| fn from(val: u32) -> Self { |
| if val & 0x7F80_0000 == 0x7F80_0000 { |
| if val & 0x8000_0000 == 0 { |
| Self::Start((val & 0x3F_FFFF) as usize) |
| } else { |
| Self::End((val & 0x3F_FFFF) as usize, val & 0x40_0000 != 0) |
| } |
| } else { |
| Self::Incr(f32::from_bits(val)) |
| } |
| } |
| } |
| |
| impl From<PointCommand> for u32 { |
| fn from(command: PointCommand) -> Self { |
| match command { |
| PointCommand::Start(i) => 0x7F80_0000 | (i as u32 & 0x3F_FFFF), |
| PointCommand::Incr(point_command) => point_command.to_bits(), |
| PointCommand::End(i, new_contour) => { |
| 0xFF80_0000 | (i as u32 & 0x3F_FFFF) | (u32::from(new_contour) << 22) |
| } |
| } |
| } |
| } |
| |
| #[derive(Clone, Debug)] |
| struct Contour; |
| |
| #[derive(Clone, Debug)] |
| struct Spline { |
| curvature: f32, |
| p0: Point, |
| p2: Point, |
| contour: Option<Contour>, |
| } |
| |
| impl Spline { |
| #[inline] |
| pub fn new_spline_needed(&mut self, angle_changed: bool, point: Point) -> Option<Contour> { |
| let needed = angle_changed || (point - self.p2).len() >= MAX_ERROR; |
| |
| needed.then(|| self.contour.take()).flatten() |
| } |
| } |
| |
| #[derive(Clone, Debug)] |
| struct Primitives { |
| last_angle: Option<f32>, |
| contour: Option<Contour>, |
| splines: Vec<Spline>, |
| x: Vec<f32>, |
| y: Vec<f32>, |
| weight: Vec<f32>, |
| x0: Vec<f32>, |
| dx_recip: Vec<f32>, |
| k0: Vec<f32>, |
| dk: Vec<f32>, |
| curvatures_recip: Vec<f32>, |
| partial_curvatures: Vec<(u32, f32)>, |
| } |
| |
| impl Primitives { |
| #[inline] |
| fn last_spline_or_insert_with<F>( |
| &mut self, |
| angle: Option<f32>, |
| point: Point, |
| f: F, |
| ) -> &mut Spline |
| where |
| F: FnOnce(Contour) -> Spline, |
| { |
| if let Some(contour) = self.contour.take().or_else(|| { |
| fn diff(a0: f32, a1: f32) -> f32 { |
| let mut diff = (a1 - a0).abs(); |
| |
| if diff > f32::consts::PI { |
| diff -= f32::consts::PI; |
| } |
| |
| if diff > f32::consts::FRAC_PI_2 { |
| diff = f32::consts::PI - diff; |
| } |
| |
| diff |
| } |
| |
| let angle_changed = if let (Some(a0), Some(a1)) = (self.last_angle, angle) { |
| diff(a0, a1) > MAX_ANGLE_ERROR |
| } else { |
| false |
| }; |
| |
| self.splines |
| .last_mut() |
| .and_then(|spline| spline.new_spline_needed(angle_changed, point)) |
| }) { |
| self.splines.push(f(contour)); |
| } |
| |
| self.splines.last_mut().unwrap() |
| } |
| |
| pub fn push_contour(&mut self, contour: Contour) { |
| self.contour = Some(contour); |
| } |
| |
| pub fn push_line(&mut self, points: [WeightedPoint; 2]) { |
| let p0 = points[0].applied(); |
| let p1 = points[1].applied(); |
| |
| let d = p1 - p0; |
| let angle = d.angle(); |
| |
| let spline = self.last_spline_or_insert_with(angle, p0, |contour| Spline { |
| curvature: 0.0, |
| p0, |
| p2: p1, |
| contour: Some(contour), |
| }); |
| |
| spline.p2 = p1; |
| |
| self.last_angle = angle; |
| } |
| |
| pub fn push_quad(&mut self, points: [WeightedPoint; 3]) { |
| const PIXEL_ACCURACY_RECIP: f32 = 1.0 / MAX_ERROR; |
| |
| let p0 = points[0].applied(); |
| let p1 = points[1].applied(); |
| let p2 = points[2].applied(); |
| |
| let a = p1 - p0; |
| let b = p2 - p1; |
| |
| let in_angle = a.angle(); |
| let out_angle = b.angle(); |
| |
| if in_angle.is_none() && out_angle.is_none() { |
| return; |
| } |
| |
| if in_angle.is_none() || out_angle.is_none() { |
| return self.push_line([points[0], points[2]]); |
| } |
| |
| self.x.extend(points.iter().map(|p| p.point.x)); |
| self.y.extend(points.iter().map(|p| p.point.y)); |
| self.weight.extend(points.iter().map(|p| p.weight)); |
| |
| let spline = self.last_spline_or_insert_with(in_angle, p0, |contour| Spline { |
| curvature: 0.0, |
| p0, |
| p2, |
| contour: Some(contour), |
| }); |
| |
| spline.p2 = p2; |
| |
| let h = a - b; |
| |
| let cross = (p2.x - p0.x).mul_add(h.y, -(p2.y - p0.y) * h.x); |
| let cross_recip = cross.recip(); |
| |
| let mut x0 = a.x.mul_add(h.x, a.y * h.y) * cross_recip; |
| let x2 = b.x.mul_add(h.x, b.y * h.y) * cross_recip; |
| let mut dx_recip = (x2 - x0).recip(); |
| |
| let scale = (cross / (h.len() * (x2 - x0))).abs(); |
| |
| let mut k0 = curvature(x0); |
| let k2 = curvature(x2); |
| |
| let mut dk = k2 - k0; |
| let mut current_curvature = 0.5 * dk.abs() * (scale * PIXEL_ACCURACY_RECIP).sqrt(); |
| |
| // Points are collinear. |
| if !current_curvature.is_finite() || current_curvature <= 1.0 { |
| // These values are chosen such that the resulting points will be found at t = 0.5 and |
| // t = 1.0. |
| x0 = 0.036_624_67; |
| dx_recip = 1.0; |
| k0 = 0.0; |
| dk = 1.0; |
| |
| current_curvature = 2.0; |
| } |
| |
| let total_curvature = spline.curvature + current_curvature; |
| |
| spline.curvature = total_curvature; |
| |
| self.last_angle = out_angle; |
| |
| self.x0.push(x0); |
| self.dx_recip.push(dx_recip); |
| self.k0.push(k0); |
| self.dk.push(dk); |
| self.curvatures_recip.push(current_curvature.recip()); |
| self.partial_curvatures.push((self.splines.len() as u32 - 1, total_curvature)); |
| } |
| |
| pub fn push_cubic(&mut self, points: [WeightedPoint; 4]) { |
| const MAX_CUBIC_ERROR_SQUARED: f32 = (36.0 * 36.0 / 3.0) * MAX_ERROR * MAX_ERROR; |
| |
| let p0 = points[0].applied(); |
| let p1 = points[1].applied(); |
| let p2 = points[2].applied(); |
| |
| let dx = p2.x.mul_add(3.0, -p0.x) - p1.x.mul_add(3.0, -p1.x); |
| let dy = p2.y.mul_add(3.0, -p0.y) - p1.y.mul_add(3.0, -p1.y); |
| |
| let err = dx.mul_add(dx, dy * dy); |
| |
| let mult = points[1].weight.max(points[2].weight).max(1.0); |
| |
| let subdivisions = (((err * MAX_CUBIC_ERROR_SQUARED.recip()).powf(1.0 / 6.0) * mult).ceil() |
| as usize) |
| .max(1); |
| let incr = (subdivisions as f32).recip(); |
| |
| let mut quad_p0 = p0; |
| for i in 1..=subdivisions { |
| let t = i as f32 * incr; |
| |
| let quad_p2 = eval_cubic(t, &points).applied(); |
| |
| let mid_point = eval_cubic(t - 0.5 * incr, &points).applied(); |
| |
| let quad_p1 = Point { |
| x: mid_point.x.mul_add(2.0, -0.5 * (quad_p0.x + quad_p2.x)), |
| y: mid_point.y.mul_add(2.0, -0.5 * (quad_p0.y + quad_p2.y)), |
| }; |
| |
| self.push_quad([ |
| WeightedPoint { point: quad_p0, weight: 1.0 }, |
| WeightedPoint { point: quad_p1, weight: 1.0 }, |
| WeightedPoint { point: quad_p2, weight: 1.0 }, |
| ]); |
| |
| quad_p0 = quad_p2; |
| } |
| } |
| |
| pub fn populate_buffers(&self, buffers: &mut ScratchBuffers) { |
| buffers.clear(); |
| |
| let mut i = 0; |
| let mut last_spline = None; |
| for (spline_i, spline) in self.splines.iter().enumerate() { |
| let subdivisions = spline.curvature.ceil() as usize; |
| let point_command = spline.curvature / subdivisions as f32; |
| |
| let needs_start_point = last_spline.map_or(true, |last_spline: &Spline| { |
| last_spline.contour.is_some() || (last_spline.p2 - spline.p0).len() > MAX_ERROR |
| }); |
| |
| if needs_start_point { |
| buffers.point_indices.push(Default::default()); |
| buffers.quad_indices.push(Default::default()); |
| buffers.point_commands.push(PointCommand::Start(spline_i).into()); |
| } |
| |
| for pi in 1..subdivisions { |
| if pi as f32 > self.partial_curvatures[i].1 { |
| i += 1; |
| } |
| |
| buffers.point_indices.push(pi); |
| buffers.quad_indices.push(i); |
| buffers.point_commands.push(PointCommand::Incr(point_command).into()); |
| } |
| |
| buffers.point_indices.push(Default::default()); |
| buffers.quad_indices.push(Default::default()); |
| buffers |
| .point_commands |
| .push(PointCommand::End(spline_i, spline.contour.is_some()).into()); |
| |
| last_spline = Some(spline); |
| |
| if subdivisions > 0 { |
| i += 1; |
| } |
| } |
| } |
| |
| pub fn eval_quad(&self, quad_index: usize, t: f32) -> Point { |
| let i0 = 3 * quad_index; |
| let i1 = i0 + 1; |
| let i2 = i0 + 2; |
| |
| let weight = lerp( |
| t, |
| lerp(t, self.weight[i0], self.weight[i1]), |
| lerp(t, self.weight[i1], self.weight[i2]), |
| ); |
| let w_recip = weight.recip(); |
| |
| let x = lerp(t, lerp(t, self.x[i0], self.x[i1]), lerp(t, self.x[i1], self.x[i2])) * w_recip; |
| let y = lerp(t, lerp(t, self.y[i0], self.y[i1]), lerp(t, self.y[i1], self.y[i2])) * w_recip; |
| |
| Point::new(x, y) |
| } |
| |
| pub fn into_lines(self) -> Lines { |
| let mut lines = Lines::default(); |
| |
| thread_local!(static BUFFERS: RefCell<ScratchBuffers> = RefCell::new(ScratchBuffers { |
| point_indices: Vec::new(), |
| quad_indices: Vec::new(), |
| point_commands: Vec::new(), |
| })); |
| |
| BUFFERS.with(|buffers| { |
| let mut buffers = buffers.borrow_mut(); |
| |
| self.populate_buffers(&mut buffers); |
| |
| let points = buffers |
| .point_indices |
| .par_iter() |
| .with_min_len(MIN_LEN) |
| .zip(buffers.quad_indices.par_iter().with_min_len(MIN_LEN)) |
| .zip(buffers.point_commands.par_iter().with_min_len(MIN_LEN)) |
| .map(|((&pi, &qi), &point_command)| { |
| let incr = match PointCommand::from(point_command) { |
| PointCommand::Start(spline_i) => { |
| let point = self.splines[spline_i as usize].p0; |
| return ((point.x, point.y), false); |
| } |
| PointCommand::End(spline_i, new_contour) => { |
| let point = self.splines[spline_i as usize].p2; |
| return ((point.x, point.y), new_contour); |
| } |
| PointCommand::Incr(incr) => incr, |
| }; |
| |
| let spline_i = self.partial_curvatures[qi].0; |
| |
| let previous_curvature = qi |
| .checked_sub(1) |
| .and_then(|i| { |
| let partial_curvature = self.partial_curvatures[i]; |
| (partial_curvature.0 == spline_i).then(|| partial_curvature.1) |
| }) |
| .unwrap_or_default(); |
| let ratio = |
| incr.mul_add(pi as f32, -previous_curvature) * self.curvatures_recip[qi]; |
| |
| let x = inv_curvature(ratio.mul_add(self.dk[qi], self.k0[qi])); |
| |
| let t = ((x - self.x0[qi]) * self.dx_recip[qi]).clamp(0.0, 1.0); |
| |
| let point = self.eval_quad(qi, t); |
| |
| ((point.x, point.y), false) |
| }); |
| |
| ( |
| (ExtendVec::new(&mut lines.x), ExtendVec::new(&mut lines.y)), |
| ExtendVec::new(&mut lines.start_new_contour), |
| ) |
| .par_extend(points); |
| }); |
| |
| lines |
| } |
| } |
| |
| impl Default for Primitives { |
| fn default() -> Self { |
| Self { |
| last_angle: None, |
| contour: Some(Contour), |
| splines: Default::default(), |
| x: Default::default(), |
| y: Default::default(), |
| weight: Default::default(), |
| x0: Default::default(), |
| dx_recip: Default::default(), |
| k0: Default::default(), |
| dk: Default::default(), |
| curvatures_recip: Default::default(), |
| partial_curvatures: Default::default(), |
| } |
| } |
| } |
| |
| #[derive(Clone, Copy, Debug, PartialEq)] |
| enum PathCommand { |
| Move, |
| Line, |
| Quad, |
| Cubic, |
| } |
| #[derive(Debug, Default)] |
| struct Lines { |
| x: Vec<f32>, |
| y: Vec<f32>, |
| start_new_contour: Vec<bool>, |
| } |
| |
| #[derive(Debug)] |
| struct PathData { |
| x: Vec<f32>, |
| y: Vec<f32>, |
| weight: Vec<f32>, |
| commands: Vec<PathCommand>, |
| open_point_index: usize, |
| lines: Option<Lines>, |
| } |
| |
| macro_rules! points { |
| ( $inner:expr , $i:expr , $( $d:expr ),+ $( , )? ) => {[ |
| $( |
| WeightedPoint { |
| point: Point::new($inner.x[$i - $d], $inner.y[$i - $d]), |
| weight: $inner.weight[$i - $d], |
| }, |
| )* |
| ]}; |
| } |
| |
| impl PathData { |
| pub fn close(&mut self) { |
| let len = self.x.len(); |
| |
| let last_point = WeightedPoint { |
| point: Point::new(self.x[len - 1], self.y[len - 1]), |
| weight: self.weight[len - 1], |
| }; |
| let open_point = WeightedPoint { |
| point: Point::new(self.x[self.open_point_index], self.y[self.open_point_index]), |
| weight: self.weight[self.open_point_index], |
| }; |
| |
| if last_point.applied() != open_point.applied() { |
| self.x.push(open_point.point.x); |
| self.y.push(open_point.point.y); |
| self.weight.push(open_point.weight); |
| |
| self.commands.push(PathCommand::Line); |
| } |
| } |
| |
| pub fn lines(&mut self) -> &Lines { |
| if self.lines.is_none() { |
| let mut primitives = Primitives::default(); |
| |
| let mut i = 0; |
| for command in &self.commands { |
| match command { |
| PathCommand::Move => { |
| i += 1; |
| |
| primitives.push_contour(Contour); |
| } |
| PathCommand::Line => { |
| i += 1; |
| |
| let points = points!(self, i, 2, 1); |
| primitives.push_line(points); |
| } |
| PathCommand::Quad => { |
| i += 2; |
| |
| let points = points!(self, i, 3, 2, 1); |
| primitives.push_quad(points); |
| } |
| PathCommand::Cubic => { |
| i += 3; |
| |
| let points = points!(self, i, 4, 3, 2, 1); |
| primitives.push_cubic(points); |
| } |
| } |
| } |
| |
| self.lines = Some(primitives.into_lines()); |
| } |
| |
| self.lines.as_ref().unwrap() |
| } |
| } |
| |
| impl Default for PathData { |
| fn default() -> Self { |
| Self { |
| x: vec![0.0], |
| y: vec![0.0], |
| weight: vec![1.0], |
| commands: vec![PathCommand::Move], |
| open_point_index: 0, |
| lines: None, |
| } |
| } |
| } |
| |
| #[derive(Clone, Debug, Default)] |
| pub struct Path { |
| inner: Rc<RefCell<PathData>>, |
| transform: Option<GeomPresTransform>, |
| } |
| |
| impl Path { |
| pub(crate) fn push_lines_to( |
| &self, |
| x: &mut Vec<f32>, |
| y: &mut Vec<f32>, |
| id: GeomId, |
| ids: &mut Vec<Option<GeomId>>, |
| ) { |
| let mut inner = self.inner.borrow_mut(); |
| |
| let lines = inner.lines(); |
| let transform = &self.transform; |
| |
| if let Some(transform) = &transform { |
| let iter = lines |
| .x |
| .par_iter() |
| .with_min_len(MIN_LEN) |
| .zip( |
| lines |
| .y |
| .par_iter() |
| .with_min_len(MIN_LEN) |
| .zip(lines.start_new_contour.par_iter().with_min_len(MIN_LEN)), |
| ) |
| .map(|(&x, (&y, start_new_contour))| { |
| let point = transform.transform(Point::new(x, y)); |
| (point.x, point.y, (!start_new_contour).then(|| id)) |
| }); |
| |
| ExtendTuple3::new((x, y, ids)).par_extend(iter); |
| } else { |
| let iter = lines |
| .x |
| .par_iter() |
| .with_min_len(MIN_LEN) |
| .zip( |
| lines |
| .y |
| .par_iter() |
| .with_min_len(MIN_LEN) |
| .zip(lines.start_new_contour.par_iter().with_min_len(MIN_LEN)), |
| ) |
| .map(|(&x, (&y, start_new_contour))| (x, y, (!start_new_contour).then(|| id))); |
| |
| ExtendTuple3::new((x, y, ids)).par_extend(iter); |
| } |
| } |
| |
| #[inline] |
| pub fn transform(&self, transform: &[f32; 9]) -> Self { |
| if let Some(transform) = GeomPresTransform::new(*transform) { |
| return Self { inner: Rc::clone(&self.inner), transform: Some(transform) }; |
| } |
| |
| let inner = self.inner.borrow(); |
| let mut data = PathData { |
| x: inner.x.clone(), |
| y: inner.y.clone(), |
| weight: inner.weight.clone(), |
| commands: inner.commands.clone(), |
| open_point_index: inner.open_point_index, |
| lines: None, |
| }; |
| |
| data.x |
| .par_iter_mut() |
| .with_min_len(MIN_LEN) |
| .zip( |
| data.y |
| .par_iter_mut() |
| .with_min_len(MIN_LEN) |
| .zip(data.weight.par_iter_mut().with_min_len(MIN_LEN)), |
| ) |
| .for_each(|(x, (y, weight))| { |
| (*x, *y, *weight) = ( |
| transform[0].mul_add(*x, transform[1].mul_add(*y, transform[2] * *weight)), |
| transform[3].mul_add(*x, transform[4].mul_add(*y, transform[5] * *weight)), |
| transform[6].mul_add(*x, transform[7].mul_add(*y, transform[8] * *weight)), |
| ); |
| }); |
| |
| Self { inner: Rc::new(RefCell::new(data)), transform: None } |
| } |
| } |
| |
| impl Eq for Path {} |
| |
| impl PartialEq for Path { |
| fn eq(&self, other: &Self) -> bool { |
| Rc::ptr_eq(&self.inner, &other.inner) |
| } |
| } |
| |
| #[derive(Clone, Debug, Default)] |
| pub struct PathBuilder { |
| inner: Rc<RefCell<PathData>>, |
| } |
| |
| impl PathBuilder { |
| #[inline] |
| pub fn new() -> Self { |
| Self::default() |
| } |
| |
| #[inline] |
| pub fn move_to(&mut self, p: Point) -> &mut Self { |
| { |
| let mut inner = self.inner.borrow_mut(); |
| let len = inner.x.len(); |
| |
| if matches!(inner.commands[inner.commands.len() - 1], PathCommand::Move) { |
| inner.x[len - 1] = p.x; |
| inner.y[len - 1] = p.y; |
| inner.weight[len - 1] = 1.0; |
| } else { |
| inner.close(); |
| |
| let open_point_index = inner.x.len(); |
| |
| inner.x.push(p.x); |
| inner.y.push(p.y); |
| inner.weight.push(1.0); |
| |
| inner.commands.push(PathCommand::Move); |
| |
| inner.open_point_index = open_point_index; |
| } |
| } |
| |
| self |
| } |
| |
| #[inline] |
| pub fn line_to(&mut self, p: Point) -> &mut Self { |
| { |
| let mut inner = self.inner.borrow_mut(); |
| |
| inner.x.push(p.x); |
| inner.y.push(p.y); |
| inner.weight.push(1.0); |
| |
| inner.commands.push(PathCommand::Line); |
| } |
| |
| self |
| } |
| |
| #[inline] |
| pub fn quad_to(&mut self, p1: Point, p2: Point) -> &mut Self { |
| { |
| let mut inner = self.inner.borrow_mut(); |
| |
| inner.x.push(p1.x); |
| inner.y.push(p1.y); |
| inner.weight.push(1.0); |
| |
| inner.x.push(p2.x); |
| inner.y.push(p2.y); |
| inner.weight.push(1.0); |
| |
| inner.commands.push(PathCommand::Quad); |
| } |
| |
| self |
| } |
| |
| #[inline] |
| pub fn cubic_to(&mut self, p1: Point, p2: Point, p3: Point) -> &mut Self { |
| { |
| let mut inner = self.inner.borrow_mut(); |
| |
| inner.x.push(p1.x); |
| inner.y.push(p1.y); |
| inner.weight.push(1.0); |
| |
| inner.x.push(p2.x); |
| inner.y.push(p2.y); |
| inner.weight.push(1.0); |
| |
| inner.x.push(p3.x); |
| inner.y.push(p3.y); |
| inner.weight.push(1.0); |
| |
| inner.commands.push(PathCommand::Cubic); |
| } |
| |
| self |
| } |
| |
| #[inline] |
| pub fn rat_quad_to(&mut self, p1: Point, p2: Point, weight: f32) -> &mut Self { |
| { |
| let mut inner = self.inner.borrow_mut(); |
| |
| inner.x.push(p1.x * weight); |
| inner.y.push(p1.y * weight); |
| inner.weight.push(weight); |
| |
| inner.x.push(p2.x); |
| inner.y.push(p2.y); |
| inner.weight.push(1.0); |
| |
| inner.commands.push(PathCommand::Quad); |
| } |
| |
| self |
| } |
| |
| #[inline] |
| pub fn rat_cubic_to(&mut self, p1: Point, p2: Point, p3: Point, w1: f32, w2: f32) -> &mut Self { |
| { |
| let mut inner = self.inner.borrow_mut(); |
| |
| inner.x.push(p1.x * w1); |
| inner.y.push(p1.y * w1); |
| inner.weight.push(w1); |
| |
| inner.x.push(p2.x * w2); |
| inner.y.push(p2.y * w2); |
| inner.weight.push(w2); |
| |
| inner.x.push(p3.x); |
| inner.y.push(p3.y); |
| inner.weight.push(1.0); |
| |
| inner.commands.push(PathCommand::Cubic); |
| } |
| |
| self |
| } |
| |
| #[inline] |
| pub fn build(&mut self) -> Path { |
| let mut inner = self.inner.borrow_mut(); |
| |
| inner.close(); |
| |
| Path { inner: Rc::clone(&self.inner), transform: None } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| fn dist(p0: Point, p1: Point, p2: Point) -> f32 { |
| let d10 = p1 - p0; |
| let d21 = p2 - p1; |
| |
| (d21.x * d10.y - d10.x * d21.y).abs() / d21.len() |
| } |
| |
| fn min_dist(p: Point, points: &[Point]) -> f32 { |
| points |
| .windows(2) |
| .map(|window| dist(p, window[0], window[1])) |
| .min_by(|a, b| a.partial_cmp(b).unwrap()) |
| .unwrap() |
| } |
| |
| fn eval_quad(t: f32, points: &[WeightedPoint; 3]) -> Point { |
| let x = lerp( |
| t, |
| lerp(t, points[0].point.x, points[1].point.x), |
| lerp(t, points[1].point.x, points[2].point.x), |
| ); |
| let y = lerp( |
| t, |
| lerp(t, points[0].point.y, points[1].point.y), |
| lerp(t, points[1].point.y, points[2].point.y), |
| ); |
| |
| Point { x, y } |
| } |
| |
| macro_rules! curve { |
| ( |
| ( $p0x:expr , $p0y:expr ) , |
| ( $p1x:expr , $p1y:expr ) , |
| ( $p2x:expr , $p2y:expr ) , |
| ( $p3x:expr , $p3y:expr ) $( , )? |
| ) => { |
| [ |
| WeightedPoint { point: Point::new($p0x, $p0y), weight: 1.0 }, |
| WeightedPoint { point: Point::new($p1x, $p1y), weight: 1.0 }, |
| WeightedPoint { point: Point::new($p2x, $p2y), weight: 1.0 }, |
| WeightedPoint { point: Point::new($p3x, $p3y), weight: 1.0 }, |
| ] |
| }; |
| ( |
| ( $p0x:expr , $p0y:expr ) , |
| ( $p1x:expr , $p1y:expr ) , |
| ( $p2x:expr , $p2y:expr ) $( , )? |
| ) => { |
| [ |
| WeightedPoint { point: Point::new($p0x, $p0y), weight: 1.0 }, |
| WeightedPoint { point: Point::new($p1x, $p1y), weight: 1.0 }, |
| WeightedPoint { point: Point::new($p2x, $p2y), weight: 1.0 }, |
| ] |
| }; |
| ( ( $p0x:expr , $p0y:expr ) , ( $p1x:expr , $p1y:expr ) $( , )? ) => { |
| [ |
| WeightedPoint { point: Point::new($p0x, $p0y), weight: 1.0 }, |
| WeightedPoint { point: Point::new($p1x, $p1y), weight: 1.0 }, |
| ] |
| }; |
| } |
| |
| #[test] |
| fn quads() { |
| let mut primitives = Primitives::default(); |
| |
| let c0 = curve![(2.0, 0.0), (0.0, 1.0), (10.0, 1.0)]; |
| let c1 = curve![(10.0, 1.0), (20.0, 1.0), (18.0, 0.0)]; |
| |
| primitives.push_quad(c0); |
| primitives.push_quad(c1); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 9); |
| |
| assert_eq!(lines.x[0], 2.0); |
| assert_eq!(lines.y[0], 0.0); |
| assert_eq!(lines.x[8], 18.0); |
| assert_eq!(lines.y[8], 0.0); |
| |
| let a = Point::new(lines.x[3], lines.y[3]); |
| let b = Point::new(lines.x[5], lines.y[5]); |
| |
| assert!((a - b).len() > 10.0); |
| |
| let points: Vec<_> = |
| lines.x.iter().zip(lines.y.iter()).map(|(&x, &y)| Point::new(x, y)).collect(); |
| |
| let min_c0 = (0..=50) |
| .into_iter() |
| .map(|i| { |
| let t = i as f32 / 50.0; |
| |
| min_dist(eval_quad(t, &c0), &points) |
| }) |
| .max_by(|a, b| a.partial_cmp(b).unwrap()) |
| .unwrap(); |
| let min_c1 = (0..=50) |
| .into_iter() |
| .map(|i| { |
| let t = i as f32 / 50.0; |
| |
| min_dist(eval_quad(t, &c1), &points) |
| }) |
| .max_by(|a, b| a.partial_cmp(b).unwrap()) |
| .unwrap(); |
| |
| assert!(min_c0 < MAX_ERROR); |
| assert!(min_c1 < MAX_ERROR); |
| } |
| |
| #[test] |
| fn two_splines() { |
| let mut primitives = Primitives::default(); |
| |
| primitives.push_quad(curve![(0.0, 0.0), (1.0, 2.0), (2.0, 0.0)]); |
| primitives.push_quad(curve![(3.0, 0.0), (4.0, 4.0), (5.0, 0.0)]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 11); |
| |
| assert_eq!(lines.x[0], 0.0); |
| assert_eq!(lines.y[0], 0.0); |
| assert_eq!(lines.x[4], 2.0); |
| assert_eq!(lines.y[4], 0.0); |
| assert_eq!(lines.x[5], 3.0); |
| assert_eq!(lines.y[5], 0.0); |
| assert_eq!(lines.x[10], 5.0); |
| assert_eq!(lines.y[10], 0.0); |
| } |
| |
| #[test] |
| fn collinear_quad() { |
| let mut primitives = Primitives::default(); |
| |
| primitives.push_quad(curve![(0.0, 0.0), (2.0, 0.0001), (1.0, 0.0)]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 3); |
| |
| assert!((lines.x[1] - 1.25).abs() < 0.01); |
| assert!((lines.y[1] - 0.0).abs() < 0.01); |
| } |
| |
| #[test] |
| fn overlapping_control_point_quad() { |
| let mut primitives = Primitives::default(); |
| |
| primitives.push_quad(curve![(0.0, 0.0), (0.0, 0.0), (1.0, 1.0)]); |
| primitives.push_quad(curve![(1.0, 1.0), (1.0, 1.0), (1.0, 1.0)]); |
| primitives.push_quad(curve![(1.0, 1.0), (2.0, 2.0), (2.0, 2.0)]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 2); |
| |
| assert!((lines.x[0] - 0.0).abs() < 0.01); |
| assert!((lines.y[0] - 0.0).abs() < 0.01); |
| assert!((lines.x[1] - 2.0).abs() < 0.01); |
| assert!((lines.y[1] - 2.0).abs() < 0.01); |
| } |
| |
| #[test] |
| fn rat_quad() { |
| let mut primitives = Primitives::default(); |
| let weight = 10.0; |
| |
| primitives.push_quad([ |
| WeightedPoint { point: Point::new(0.0, 0.0), weight: 1.0 }, |
| WeightedPoint { point: Point::new(1.0 * weight, 2.0 * weight), weight }, |
| WeightedPoint { point: Point::new(2.0, 0.0), weight: 1.0 }, |
| ]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 5); |
| |
| let points: Vec<_> = |
| lines.x.iter().zip(lines.y.iter()).map(|(&x, &y)| Point::new(x, y)).collect(); |
| |
| assert!((points[2].x - 1.0).abs() <= 0.001); |
| |
| let distances: Vec<_> = |
| points.windows(2).map(|window| (window[1] - window[0]).len()).collect(); |
| |
| assert!(distances[0] > 1.5); |
| assert!(distances[1] < 0.2); |
| assert!(distances[2] < 0.2); |
| assert!(distances[3] > 1.5); |
| } |
| |
| #[test] |
| fn lines_and_quads() { |
| let mut primitives = Primitives::default(); |
| |
| primitives.push_line(curve![(-1.0, -2.0), (0.0, 0.0)]); |
| primitives.push_quad(curve![(0.0, 0.0), (1.0, 2.0), (2.0, 0.0)]); |
| primitives.push_line(curve![(2.0, 0.0), (3.0, -2.0)]); |
| primitives.push_line(curve![(3.0, -2.0), (4.0, 2.0)]); |
| primitives.push_line(curve![(4.0, 2.0), (5.0, -4.0)]); |
| primitives.push_line(curve![(5.0, -4.0), (6.0, 0.0)]); |
| primitives.push_quad(curve![(6.0, 0.0), (7.0, 4.0), (8.0, 0.0)]); |
| primitives.push_line(curve![(8.0, 0.0), (9.0, -4.0)]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 12); |
| |
| assert_eq!(lines.x[0], -1.0); |
| assert_eq!(lines.y[0], -2.0); |
| assert_eq!(lines.x[4], 3.0); |
| assert_eq!(lines.y[4], -2.0); |
| assert_eq!(lines.x[5], 4.0); |
| assert_eq!(lines.y[5], 2.0); |
| assert_eq!(lines.x[6], 5.0); |
| assert_eq!(lines.y[6], -4.0); |
| assert_eq!(lines.x[11], 9.0); |
| assert_eq!(lines.y[11], -4.0); |
| } |
| |
| #[test] |
| fn cubic() { |
| let mut primitives = Primitives::default(); |
| |
| primitives.push_cubic(curve![(0.0, 0.0), (10.0, 6.0), (-2.0, 6.0), (8.0, 0.0)]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 10); |
| |
| assert!(lines.x[2] > lines.x[7]); |
| assert!(lines.x[3] > lines.x[6]); |
| assert!(lines.x[4] > lines.x[5]); |
| |
| assert!(lines.y[0] < lines.y[1]); |
| assert!(lines.y[1] < lines.y[2]); |
| assert!(lines.y[2] < lines.y[3]); |
| assert!(lines.y[3] < lines.y[4]); |
| |
| assert!(lines.y[5] > lines.y[6]); |
| assert!(lines.y[6] > lines.y[7]); |
| assert!(lines.y[7] > lines.y[8]); |
| assert!(lines.y[8] > lines.y[9]); |
| } |
| |
| #[test] |
| fn rat_cubic_high() { |
| let mut primitives = Primitives::default(); |
| let weight = 10.0; |
| |
| primitives.push_cubic([ |
| WeightedPoint { point: Point::new(0.0, 0.0), weight: 1.0 }, |
| WeightedPoint { point: Point::new(5.0 * weight, 3.0 * weight), weight }, |
| WeightedPoint { point: Point::new(-1.0 * weight, 3.0 * weight), weight }, |
| WeightedPoint { point: Point::new(4.0, 0.0), weight: 1.0 }, |
| ]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 45); |
| } |
| |
| #[test] |
| fn rat_cubic_low() { |
| let mut primitives = Primitives::default(); |
| let weight = 0.5; |
| |
| primitives.push_cubic([ |
| WeightedPoint { point: Point::new(0.0, 0.0), weight: 1.0 }, |
| WeightedPoint { point: Point::new(5.0 * weight, 3.0 * weight), weight }, |
| WeightedPoint { point: Point::new(-1.0 * weight, 3.0 * weight), weight }, |
| WeightedPoint { point: Point::new(4.0, 0.0), weight: 1.0 }, |
| ]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 7); |
| } |
| |
| #[test] |
| fn collinear_cubic() { |
| let mut primitives = Primitives::default(); |
| |
| primitives.push_cubic(curve![(1.0, 0.0), (0.0, 0.0), (3.0, 0.0), (2.0, 0.0)]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 5); |
| |
| assert_eq!(lines.x[0], 1.0); |
| assert_eq!(lines.y[0], 0.0); |
| |
| assert!(lines.x[1] > 0.5); |
| assert!(lines.x[1] < 1.0); |
| assert_eq!(lines.y[1], 0.0); |
| |
| assert!(lines.x[2] > 1.0); |
| assert!(lines.x[2] < 2.0); |
| assert_eq!(lines.y[2], 0.0); |
| |
| assert!(lines.x[3] > 2.0); |
| assert!(lines.x[3] < 2.5); |
| assert_eq!(lines.y[3], 0.0); |
| |
| assert_eq!(lines.x[4], 2.0); |
| assert_eq!(lines.y[4], 0.0); |
| } |
| |
| #[test] |
| fn overlapping_control_point_cubic_line() { |
| let mut primitives = Primitives::default(); |
| |
| primitives.push_cubic(curve![(0.0, 0.0), (0.0, 0.0), (1.0, 1.0), (1.0, 1.0)]); |
| primitives.push_cubic(curve![(1.0, 1.0), (1.0, 1.0), (1.0, 1.0), (1.0, 1.0)]); |
| primitives.push_cubic(curve![(1.0, 1.0), (1.0, 1.0), (2.0, 2.0), (2.0, 2.0)]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 9); |
| |
| for x in lines.x.windows(2) { |
| assert!(x[0] < x[1]); |
| } |
| |
| for y in lines.y.windows(2) { |
| assert!(y[0] < y[1]); |
| } |
| |
| for (&x, &y) in lines.x.iter().zip(lines.y.iter()) { |
| assert_eq!(x, y); |
| } |
| |
| assert!((lines.x[0] - 0.0).abs() < 0.01); |
| assert!((lines.y[0] - 0.0).abs() < 0.01); |
| assert!((lines.x[8] - 2.0).abs() < 0.01); |
| assert!((lines.y[8] - 2.0).abs() < 0.01); |
| } |
| |
| #[test] |
| fn ring() { |
| let mut primitives = Primitives::default(); |
| |
| primitives.push_cubic(curve![(0.0, 2.0), (2.0, 2.0), (2.0, 2.0), (2.0, 0.0)]); |
| primitives.push_cubic(curve![(2.0, 0.0), (2.0, -2.0), (2.0, -2.0), (0.0, -2.0)]); |
| primitives.push_cubic(curve![(0.0, -2.0), (-2.0, -2.0), (-2.0, -2.0), (-2.0, 0.0)]); |
| primitives.push_cubic(curve![(-2.0, 0.0), (-2.0, 2.0), (-2.0, 2.0), (0.0, 2.0)]); |
| |
| primitives.push_contour(Contour); |
| |
| primitives.push_cubic(curve![(0.0, 1.0), (-1.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)]); |
| primitives.push_cubic(curve![(-1.0, 0.0), (-1.0, -1.0), (-1.0, -1.0), (0.0, -1.0)]); |
| primitives.push_cubic(curve![(0.0, -1.0), (1.0, -1.0), (1.0, -1.0), (1.0, 0.0)]); |
| primitives.push_cubic(curve![(1.0, 0.0), (1.0, 1.0), (1.0, 1.0), (0.0, 1.0)]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.start_new_contour.len(), 30); |
| |
| assert_eq!( |
| lines.start_new_contour.iter().filter(|&&start_new_contour| start_new_contour).count(), |
| 2 |
| ); |
| |
| assert!(lines.start_new_contour[16]); |
| assert!(lines.start_new_contour[29]); |
| } |
| |
| #[test] |
| fn ring_overlapping_start() { |
| let mut primitives = Primitives::default(); |
| |
| primitives.push_cubic(curve![(0.0, 1.0), (-1.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)]); |
| primitives.push_cubic(curve![(-1.0, 0.0), (-1.0, -1.0), (-1.0, -1.0), (0.0, -1.0)]); |
| primitives.push_cubic(curve![(0.0, -1.0), (1.0, -1.0), (1.0, -1.0), (1.0, 0.0)]); |
| primitives.push_cubic(curve![(1.0, 0.0), (1.0, 1.0), (1.0, 1.0), (0.0, 1.0)]); |
| |
| primitives.push_contour(Contour); |
| |
| primitives.push_cubic(curve![(0.0, 1.0), (1.0, 1.0), (1.0, 1.0), (1.0, 2.0)]); |
| primitives.push_cubic(curve![(1.0, 2.0), (1.0, 3.0), (1.0, 3.0), (0.0, 3.0)]); |
| primitives.push_cubic(curve![(0.0, 3.0), (-1.0, 3.0), (-1.0, 3.0), (-1.0, 2.0)]); |
| primitives.push_cubic(curve![(-1.0, 2.0), (-1.0, 1.0), (-1.0, 1.0), (0.0, 1.0)]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.start_new_contour.len(), 26); |
| |
| assert_eq!( |
| lines.start_new_contour.iter().filter(|&&start_new_contour| start_new_contour).count(), |
| 2 |
| ); |
| |
| assert!(lines.start_new_contour[12]); |
| assert!(lines.start_new_contour[25]); |
| } |
| |
| #[test] |
| fn circle() { |
| let mut primitives = Primitives::default(); |
| let radius = 50.0; |
| let weight = 2.0f32.sqrt() / 2.0; |
| |
| primitives.push_quad([ |
| WeightedPoint { point: Point::new(radius, 0.0), weight: 1.0 }, |
| WeightedPoint { point: Point::new(0.0, 0.0), weight }, |
| WeightedPoint { point: Point::new(0.0, radius), weight: 1.0 }, |
| ]); |
| primitives.push_quad([ |
| WeightedPoint { point: Point::new(0.0, radius), weight: 1.0 }, |
| WeightedPoint { point: Point::new(0.0, 2.0 * radius * weight), weight }, |
| WeightedPoint { point: Point::new(radius, 2.0 * radius), weight: 1.0 }, |
| ]); |
| primitives.push_quad([ |
| WeightedPoint { point: Point::new(radius, 2.0 * radius), weight: 1.0 }, |
| WeightedPoint { |
| point: Point::new(2.0 * radius * weight, 2.0 * radius * weight), |
| weight, |
| }, |
| WeightedPoint { point: Point::new(2.0 * radius, radius), weight: 1.0 }, |
| ]); |
| primitives.push_quad([ |
| WeightedPoint { point: Point::new(2.0 * radius, radius), weight: 1.0 }, |
| WeightedPoint { point: Point::new(2.0 * radius * weight, 0.0), weight }, |
| WeightedPoint { point: Point::new(radius, 0.0), weight: 1.0 }, |
| ]); |
| |
| let lines = primitives.into_lines(); |
| |
| assert_eq!(lines.x.len(), 66); |
| |
| let points: Vec<_> = |
| lines.x.iter().zip(lines.y.iter()).map(|(&x, &y)| Point::new(x, y)).collect(); |
| |
| let max_distance = points |
| .windows(2) |
| .map(|window| (window[1] - window[0]).len()) |
| .max_by(|a, b| a.partial_cmp(b).unwrap()) |
| .unwrap(); |
| |
| assert!(max_distance < 5.0); |
| } |
| |
| #[test] |
| fn transform_path() { |
| let weight = 2.0f32.sqrt() / 2.0; |
| let radius = 10.0; |
| |
| let mut builder = PathBuilder::new(); |
| |
| builder.move_to(Point::new(radius, 0.0)); |
| builder.rat_quad_to(Point::new(radius, -radius), Point::new(0.0, -radius), weight); |
| builder.rat_quad_to(Point::new(-radius, -radius), Point::new(-radius, 0.0), weight); |
| builder.rat_quad_to(Point::new(-radius, radius), Point::new(0.0, radius), weight); |
| builder.rat_quad_to(Point::new(radius, radius), Point::new(radius, 0.0), weight); |
| |
| let path = builder.build(); |
| |
| let mut x = Vec::new(); |
| let mut y = Vec::new(); |
| let mut ids = Vec::new(); |
| |
| let id0 = GeomId::default(); |
| let id1 = id0.next(); |
| |
| path.push_lines_to(&mut x, &mut y, id1, &mut ids); |
| |
| let orig_len = x.len(); |
| |
| assert_eq!(ids[..ids.len() - 1], vec![Some(id1); ids.len() - 1]); |
| assert_eq!(ids[ids.len() - 1], None); |
| |
| for (&x, &y) in x.iter().zip(y.iter()) { |
| assert!((Point::new(x, y).len() - radius).abs() <= 0.1); |
| } |
| |
| let dx = 5.0; |
| let dy = 20.0; |
| let translated_path = path.transform(&[1.0, 0.0, dx, 0.0, 1.0, dy, 0.0, 0.0, 1.0]); |
| |
| x.clear(); |
| y.clear(); |
| |
| translated_path.push_lines_to(&mut x, &mut y, id0, &mut ids); |
| |
| for (&x, &y) in x.iter().zip(y.iter()) { |
| assert!((Point::new(x - dx, y - dy).len() - radius).abs() <= 0.1); |
| } |
| |
| let s = 2.0; |
| let scaled_path = path.transform(&[s, 0.0, 0.0, 0.0, s, 0.0, 0.0, 0.0, 1.0]); |
| |
| x.clear(); |
| y.clear(); |
| |
| scaled_path.push_lines_to(&mut x, &mut y, id0, &mut ids); |
| |
| for (&x, &y) in x.iter().zip(y.iter()) { |
| assert!((Point::new(x, y).len() - s * radius).abs() <= 0.1); |
| } |
| |
| let scaled_len = x.len(); |
| |
| assert!(scaled_len > orig_len); |
| } |
| |
| #[test] |
| fn perspective_transform_path() { |
| let weight = 2.0f32.sqrt() / 2.0; |
| let radius = 10.0; |
| let translation = 1000.0; |
| |
| let mut builder = PathBuilder::new(); |
| |
| builder.move_to(Point::new(radius + translation, 0.0)); |
| builder.rat_quad_to( |
| Point::new(radius + translation, -radius), |
| Point::new(translation, -radius), |
| weight, |
| ); |
| builder.rat_quad_to( |
| Point::new(-radius + translation, -radius), |
| Point::new(-radius + translation, 0.0), |
| weight, |
| ); |
| builder.rat_quad_to( |
| Point::new(-radius + translation, radius), |
| Point::new(translation, radius), |
| weight, |
| ); |
| builder.rat_quad_to( |
| Point::new(radius + translation, radius), |
| Point::new(radius + translation, 0.0), |
| weight, |
| ); |
| |
| let path = builder.build().transform(&[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.001, 0.0, 1.0]); |
| |
| let mut x = Vec::new(); |
| let mut y = Vec::new(); |
| let mut ids = Vec::new(); |
| |
| path.push_lines_to(&mut x, &mut y, GeomId::default(), &mut ids); |
| |
| let mut points: Vec<_> = x.iter().zip(y.iter()).map(|(&x, &y)| Point::new(x, y)).collect(); |
| points.pop(); // Remove duplicate point. |
| |
| let half_len = points.len() / 2; |
| |
| let min = (0..half_len) |
| .into_iter() |
| .map(|i| (points[i] - points[(i + half_len) % points.len()]).len()) |
| .min_by(|a, b| a.partial_cmp(b).unwrap()) |
| .unwrap(); |
| let max = (0..half_len) |
| .into_iter() |
| .map(|i| (points[i] - points[(i + half_len) % points.len()]).len()) |
| .max_by(|a, b| a.partial_cmp(b).unwrap()) |
| .unwrap(); |
| |
| assert!((min - radius / 2.0).abs() <= 0.2); |
| assert!((max - radius).abs() <= 0.2); |
| } |
| } |