Skip to main content

motion_core/path2d/
editing.rs

1//! Chainable and fallible path-building operations, including elliptical arcs.
2
3use super::{MAX_PATH_VERBS, Path2D, PathVerb, invalid};
4use crate::{Point, ValidationError, Vector2};
5use std::sync::Arc;
6
7impl Path2D {
8    /// Starts a subpath; invalid input is reported when the path is compiled.
9    pub fn move_to(&mut self, point: Point) -> &mut Self {
10        self.author(PathVerb::MoveTo(point))
11    }
12
13    /// Starts a new subpath.
14    pub fn try_move_to(&mut self, point: Point) -> Result<(), ValidationError> {
15        self.push(PathVerb::MoveTo(point))
16    }
17
18    /// Adds a line, deferring invalid state or coordinates to compilation.
19    pub fn line_to(&mut self, point: Point) -> &mut Self {
20        self.author(PathVerb::LineTo(point))
21    }
22
23    /// Adds a line; requires a preceding move.
24    pub fn try_line_to(&mut self, point: Point) -> Result<(), ValidationError> {
25        self.push(PathVerb::LineTo(point))
26    }
27
28    /// Adds a quadratic curve, deferring invalid input to compilation.
29    pub fn quadratic_to(&mut self, control: Point, end: Point) -> &mut Self {
30        self.author(PathVerb::QuadraticTo(control, end))
31    }
32
33    /// Adds a quadratic Bezier; requires a preceding move.
34    pub fn try_quadratic_to(&mut self, control: Point, end: Point) -> Result<(), ValidationError> {
35        self.push(PathVerb::QuadraticTo(control, end))
36    }
37
38    /// Adds a cubic curve, deferring invalid input to compilation.
39    pub fn cubic_to(&mut self, first: Point, second: Point, end: Point) -> &mut Self {
40        self.author(PathVerb::CubicTo(first, second, end))
41    }
42
43    /// Adds a cubic Bezier; requires a preceding move.
44    pub fn try_cubic_to(
45        &mut self,
46        first: Point,
47        second: Point,
48        end: Point,
49    ) -> Result<(), ValidationError> {
50        self.push(PathVerb::CubicTo(first, second, end))
51    }
52
53    /// Appends an arc and retains the first invalid input for compile-time reporting.
54    pub fn arc_to(
55        &mut self,
56        center: Point,
57        radii: Vector2,
58        start_angle: f64,
59        sweep_angle: f64,
60    ) -> &mut Self {
61        if self.data.error.is_none()
62            && let Err(error) = self.try_arc_to(center, radii, start_angle, sweep_angle)
63        {
64            Arc::make_mut(&mut self.data).error = Some(error);
65        }
66        self
67    }
68
69    /// Appends a clockwise elliptical arc in radians, connecting to its start.
70    /// Radii must be finite and nonnegative. Cubics span at most pi/8 radians;
71    /// errors, including exhausted verb limits, leave the path unchanged.
72    /// Circular arcs use a cubic approximation, not rational/exact circle verbs.
73    ///
74    /// ```
75    /// use motion_core::{Path2D, Point, Vector2};
76    /// let mut path = Path2D::new();
77    /// path.arc_to(
78    ///     Point::ORIGIN,
79    ///     Vector2::new(4.0, 4.0),
80    ///     0.0,
81    ///     std::f64::consts::FRAC_PI_2,
82    /// );
83    /// assert!((path.length() - 2.0 * std::f64::consts::PI).abs() < 1e-6);
84    /// let middle = path.trim(0.25, 0.75);
85    /// assert!((middle.length() - path.length() * 0.5).abs() < 1e-7);
86    /// # Ok::<(), motion_core::ValidationError>(())
87    /// ```
88    pub fn try_arc_to(
89        &mut self,
90        center: Point,
91        radii: Vector2,
92        start_angle: f64,
93        sweep_angle: f64,
94    ) -> Result<(), ValidationError> {
95        self.elliptical_arc(center, radii, start_angle, sweep_angle, 0.0)
96    }
97
98    pub(super) fn elliptical_arc(
99        &mut self,
100        center: Point,
101        radii: Vector2,
102        start_angle: f64,
103        sweep_angle: f64,
104        rotation: f64,
105    ) -> Result<(), ValidationError> {
106        if ![
107            center.x,
108            center.y,
109            radii.x,
110            radii.y,
111            start_angle,
112            sweep_angle,
113            rotation,
114        ]
115        .iter()
116        .all(|value| value.is_finite())
117            || radii.x < 0.0
118            || radii.y < 0.0
119        {
120            return Err(invalid(
121                "arc",
122                "Arc parameters must be finite with nonnegative radii",
123            ));
124        }
125        let count = libm::ceil(sweep_angle.abs() / std::f64::consts::FRAC_PI_8);
126        if count > MAX_PATH_VERBS.saturating_sub(self.verbs().len() + 1) as f64 {
127            return Err(ValidationError::limit(
128                "path2d",
129                "verbs",
130                "Arc exceeds the verb allowance",
131            ));
132        }
133        let start = start_angle.rem_euclid(std::f64::consts::TAU);
134        let rotate = |vector: Vector2| vector.rotate(rotation);
135        let direction = |angle: f64| {
136            let angle = angle.rem_euclid(std::f64::consts::TAU);
137            Vector2::from_angle(if angle > std::f64::consts::PI {
138                angle - std::f64::consts::TAU
139            } else {
140                angle
141            })
142        };
143        let position = |angle: f64| {
144            let vector = direction(angle);
145            center + rotate(Vector2::new(radii.x * vector.x, radii.y * vector.y))
146        };
147        let tangent = |angle: f64| {
148            let vector = direction(angle);
149            rotate(Vector2::new(-radii.x * vector.y, radii.y * vector.x))
150        };
151        let mut result = self.clone();
152        let first = position(start);
153        match result.data.current {
154            None => result.try_move_to(first)?,
155            Some(current) if current != first => result.try_line_to(first)?,
156            _ => {}
157        }
158        if count > 0.0 {
159            let step = sweep_angle / count;
160            let factor = 4.0 / 3.0 * libm::tan(step / 4.0);
161            for index in 0..count as usize {
162                let angle = start + step * index as f64;
163                let next = start + step * (index + 1) as f64;
164                result.try_cubic_to(
165                    position(angle) + tangent(angle) * factor,
166                    position(next) - tangent(next) * factor,
167                    position(next),
168                )?;
169            }
170        }
171        *self = result;
172        Ok(())
173    }
174
175    /// Closes the current contour, deferring invalid state to compilation.
176    pub fn close(&mut self) -> &mut Self {
177        self.author(PathVerb::Close)
178    }
179
180    /// Closes the current subpath; requires a preceding move.
181    pub fn try_close(&mut self) -> Result<(), ValidationError> {
182        self.push(PathVerb::Close)
183    }
184
185    /// Closes and returns this path for direct styling.
186    pub fn closed(mut self) -> Self {
187        self.close();
188        self
189    }
190
191    fn author(&mut self, verb: PathVerb) -> &mut Self {
192        if let Err(error) = self.push(verb)
193            && self.data.error.is_none()
194        {
195            Arc::make_mut(&mut self.data).error = Some(error);
196        }
197        self
198    }
199}