Skip to main content

pax_runtime_api/drawing/
path_trim.rs

1use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, PathSeg, QuadBez};
2
3const PATH_TRIM_ACCURACY: f64 = 0.1;
4
5#[derive(Clone, Copy)]
6struct TrimSegment {
7    segment: PathSeg,
8    contour_index: usize,
9    closes_contour: bool,
10}
11
12#[derive(Clone, Copy)]
13struct TrimContour {
14    first_segment_index: Option<usize>,
15    last_segment_index: Option<usize>,
16    is_closed: bool,
17}
18
19/// Returns the sub-path visible between normalized `draw_start` and `draw_end`.
20pub fn trim_bez_path(path: &BezPath, draw_start: f64, draw_end: f64) -> BezPath {
21    let start = draw_start.clamp(0.0, 1.0);
22    let end = draw_end.clamp(0.0, 1.0);
23    if start <= f64::EPSILON && end >= 1.0 - f64::EPSILON {
24        return path.clone();
25    }
26    if start >= end {
27        return BezPath::new();
28    }
29
30    let (segments, contours) = collect_trim_segments(path);
31    let lengths = segments
32        .iter()
33        .map(|record| record.segment.arclen(PATH_TRIM_ACCURACY).max(0.0))
34        .collect::<Vec<_>>();
35    let total_length = lengths.iter().sum::<f64>();
36    if total_length <= f64::EPSILON {
37        return BezPath::new();
38    }
39
40    let trim_start = start * total_length;
41    let trim_end = end * total_length;
42    let mut trimmed = BezPath::new();
43    let mut current_point = None;
44    let mut cursor = 0.0;
45    let mut segment_starts = Vec::with_capacity(lengths.len());
46    let mut segment_ends = Vec::with_capacity(lengths.len());
47
48    for segment_length in &lengths {
49        segment_starts.push(cursor);
50        cursor += segment_length;
51        segment_ends.push(cursor);
52    }
53
54    let mut fully_visible_closed_contours = vec![false; contours.len()];
55    for (contour_index, contour) in contours.iter().enumerate() {
56        if !contour.is_closed {
57            continue;
58        }
59        let (Some(first_segment_index), Some(last_segment_index)) =
60            (contour.first_segment_index, contour.last_segment_index)
61        else {
62            continue;
63        };
64        let contour_start = segment_starts[first_segment_index];
65        let contour_end = segment_ends[last_segment_index];
66        fully_visible_closed_contours[contour_index] =
67            trim_start <= contour_start + f64::EPSILON && trim_end >= contour_end - f64::EPSILON;
68    }
69
70    for (index, (record, segment_length)) in segments.into_iter().zip(lengths).enumerate() {
71        let segment_start = segment_starts[index];
72        let segment_end = segment_ends[index];
73        if segment_length <= f64::EPSILON || segment_end <= trim_start + f64::EPSILON {
74            continue;
75        }
76        if segment_start >= trim_end - f64::EPSILON {
77            break;
78        }
79
80        let local_start = (trim_start - segment_start).clamp(0.0, segment_length);
81        let local_end = (trim_end - segment_start).clamp(0.0, segment_length);
82        if local_start >= local_end {
83            continue;
84        }
85
86        let whole_segment_visible =
87            local_start <= f64::EPSILON && local_end >= segment_length - f64::EPSILON;
88        let visible_segment = if whole_segment_visible {
89            record.segment
90        } else {
91            let t0 = if local_start <= f64::EPSILON {
92                0.0
93            } else {
94                record.segment.inv_arclen(local_start, PATH_TRIM_ACCURACY)
95            };
96            let t1 = if local_end >= segment_length - f64::EPSILON {
97                1.0
98            } else {
99                record.segment.inv_arclen(local_end, PATH_TRIM_ACCURACY)
100            };
101            if t0 >= t1 {
102                continue;
103            }
104            record.segment.subsegment(t0..t1)
105        };
106
107        append_segment(&mut trimmed, &mut current_point, visible_segment);
108        if whole_segment_visible
109            && record.closes_contour
110            && fully_visible_closed_contours[record.contour_index]
111        {
112            trimmed.close_path();
113            current_point = None;
114        }
115    }
116
117    trimmed
118}
119
120fn collect_trim_segments(path: &BezPath) -> (Vec<TrimSegment>, Vec<TrimContour>) {
121    let mut segments = Vec::new();
122    let mut contours = Vec::new();
123    let mut current_contour = None;
124    let mut contour_start = None;
125    let mut current_point = None;
126
127    for element in path.iter() {
128        match element {
129            kurbo::PathEl::MoveTo(point) => {
130                current_contour = Some(contours.len());
131                contours.push(TrimContour {
132                    first_segment_index: None,
133                    last_segment_index: None,
134                    is_closed: false,
135                });
136                contour_start = Some(point);
137                current_point = Some(point);
138            }
139            kurbo::PathEl::LineTo(point) => {
140                let (Some(from), Some(contour_index)) = (current_point, current_contour) else {
141                    continue;
142                };
143                push_trim_segment(
144                    &mut segments,
145                    &mut contours,
146                    contour_index,
147                    PathSeg::Line(Line::new(from, point)),
148                    false,
149                );
150                current_point = Some(point);
151            }
152            kurbo::PathEl::QuadTo(control, point) => {
153                let (Some(from), Some(contour_index)) = (current_point, current_contour) else {
154                    continue;
155                };
156                push_trim_segment(
157                    &mut segments,
158                    &mut contours,
159                    contour_index,
160                    PathSeg::Quad(QuadBez::new(from, control, point)),
161                    false,
162                );
163                current_point = Some(point);
164            }
165            kurbo::PathEl::CurveTo(control_1, control_2, point) => {
166                let (Some(from), Some(contour_index)) = (current_point, current_contour) else {
167                    continue;
168                };
169                push_trim_segment(
170                    &mut segments,
171                    &mut contours,
172                    contour_index,
173                    PathSeg::Cubic(CubicBez::new(from, control_1, control_2, point)),
174                    false,
175                );
176                current_point = Some(point);
177            }
178            kurbo::PathEl::ClosePath => {
179                let (Some(from), Some(start), Some(contour_index)) =
180                    (current_point, contour_start, current_contour)
181                else {
182                    continue;
183                };
184                contours[contour_index].is_closed = true;
185                if same_kurbo_point(from, start) {
186                    if let Some(last_segment_index) = contours[contour_index].last_segment_index {
187                        segments[last_segment_index].closes_contour = true;
188                    }
189                } else {
190                    push_trim_segment(
191                        &mut segments,
192                        &mut contours,
193                        contour_index,
194                        PathSeg::Line(Line::new(from, start)),
195                        true,
196                    );
197                    current_point = Some(start);
198                }
199            }
200        }
201    }
202
203    (segments, contours)
204}
205
206fn push_trim_segment(
207    segments: &mut Vec<TrimSegment>,
208    contours: &mut [TrimContour],
209    contour_index: usize,
210    segment: PathSeg,
211    closes_contour: bool,
212) {
213    let segment_index = segments.len();
214    let contour = &mut contours[contour_index];
215    if contour.first_segment_index.is_none() {
216        contour.first_segment_index = Some(segment_index);
217    }
218    contour.last_segment_index = Some(segment_index);
219    segments.push(TrimSegment {
220        segment,
221        contour_index,
222        closes_contour,
223    });
224}
225
226fn append_segment(path: &mut BezPath, current_point: &mut Option<kurbo::Point>, segment: PathSeg) {
227    let start = segment.start();
228    if current_point
229        .map(|point| !same_kurbo_point(point, start))
230        .unwrap_or(true)
231    {
232        path.move_to(start);
233    }
234    path.push(segment.as_path_el());
235    *current_point = Some(segment.end());
236}
237
238fn same_kurbo_point(a: kurbo::Point, b: kurbo::Point) -> bool {
239    (a.x - b.x).abs() <= f64::EPSILON && (a.y - b.y).abs() <= f64::EPSILON
240}