rive_rs/shapes/
metrics_path.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::num::NonZeroUsize;
6
7use crate::math;
8use crate::shapes::command_path::{Command, CommandPath, CommandPathBuilder};
9
10#[derive(Clone, Copy, Debug)]
11struct CubicSegment {
12    t: f32,
13    len: f32,
14}
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17enum PathPartType {
18    Line,
19    Cubic(NonZeroUsize),
20}
21
22#[derive(Clone, Copy, Debug)]
23struct PathPart {
24    r#type: PathPartType,
25    offset: usize,
26    num_segments: usize,
27}
28
29impl PathPart {
30    pub fn line(offset: usize) -> Self {
31        Self { r#type: PathPartType::Line, offset, num_segments: 0 }
32    }
33
34    pub fn cubic(offset: usize) -> Self {
35        Self { r#type: PathPartType::Cubic(NonZeroUsize::new(1).unwrap()), offset, num_segments: 0 }
36    }
37}
38
39fn compute_hull(
40    from: math::Vec,
41    from_out: math::Vec,
42    to_in: math::Vec,
43    to: math::Vec,
44    t: f32,
45    hull: &mut [math::Vec; 6],
46) {
47    hull[0] = from.lerp(from_out, t);
48    hull[1] = from_out.lerp(to_in, t);
49    hull[2] = to_in.lerp(to, t);
50
51    hull[3] = hull[0].lerp(hull[1], t);
52    hull[4] = hull[1].lerp(hull[2], t);
53
54    hull[5] = hull[3].lerp(hull[4], t);
55}
56
57fn too_far(a: math::Vec, b: math::Vec) -> bool {
58    const TOO_FAR: f32 = 1.0;
59    (a.x - b.x).abs().max((a.y - b.y).abs()) > TOO_FAR
60}
61
62fn should_split_cubic(
63    from: math::Vec,
64    from_out: math::Vec,
65    to_in: math::Vec,
66    to: math::Vec,
67) -> bool {
68    let one_third = from.lerp(to, 1.0 / 3.0);
69    let two_thirds = from.lerp(to, 2.0 / 3.0);
70
71    too_far(from_out, one_third) || too_far(to_in, two_thirds)
72}
73
74#[allow(clippy::too_many_arguments)]
75fn segment_cubic(
76    from: math::Vec,
77    from_out: math::Vec,
78    to_in: math::Vec,
79    to: math::Vec,
80    mut running_length: f32,
81    t0: f32,
82    t1: f32,
83    segments: &mut Vec<CubicSegment>,
84) -> f32 {
85    const MIN_SEGMENT_LENGTH: f32 = 0.05;
86
87    if should_split_cubic(from, from_out, to_in, to) {
88        let half_t = (t0 + t1) / 2.0;
89
90        let mut hull = [math::Vec::default(); 6];
91        compute_hull(from, from_out, to_in, to, 0.5, &mut hull);
92
93        running_length =
94            segment_cubic(from, hull[0], hull[3], hull[5], running_length, t0, half_t, segments);
95        running_length =
96            segment_cubic(hull[5], hull[4], hull[2], to, running_length, half_t, t1, segments);
97    } else {
98        let length = from.distance(to);
99        running_length += length;
100
101        if length > MIN_SEGMENT_LENGTH {
102            segments.push(CubicSegment { t: t1, len: running_length });
103        }
104    }
105
106    running_length
107}
108
109#[derive(Debug)]
110pub struct MetricsPath {
111    points: Vec<math::Vec>,
112    cubic_segments: Vec<CubicSegment>,
113    parts: Vec<PathPart>,
114    lengths: Vec<f32>,
115}
116
117impl MetricsPath {
118    pub fn new(command_path: &CommandPath) -> Self {
119        let mut points = Vec::new();
120        let mut parts = Vec::new();
121
122        for command in &command_path.commands {
123            match *command {
124                Command::MoveTo(p) => points.push(p),
125                Command::LineTo(p) => {
126                    parts.push(PathPart::line(points.len()));
127                    points.push(p);
128                }
129                Command::CubicTo(c0, c1, p) => {
130                    parts.push(PathPart::cubic(points.len()));
131                    points.push(c0);
132                    points.push(c1);
133                    points.push(p);
134                }
135                Command::Close => {
136                    if parts.last().map(|part| part.r#type) == Some(PathPartType::Line) {
137                        // We auto close the last part if it's a cubic, if it's not then make
138                        // sure to add the final part in so we can compute its length too.
139                        parts.push(PathPart::line(points.len()));
140                        points.push(points[0]);
141                    }
142                }
143            }
144        }
145
146        Self { points, cubic_segments: Vec::new(), parts, lengths: Vec::new() }
147    }
148
149    pub fn compute_length(&mut self) -> f32 {
150        self.cubic_segments.clear();
151        self.lengths.clear();
152
153        let mut i = 0;
154        let mut length = 0.0;
155
156        for part in &mut self.parts {
157            match part.r#type {
158                PathPartType::Line => {
159                    let from = self.points[i];
160                    let to = self.points[i + 1];
161
162                    i += 1;
163
164                    let part_length = from.distance(to);
165                    self.lengths.push(part_length);
166                    length += part_length;
167                }
168                PathPartType::Cubic(ref mut ci) => {
169                    let from = self.points[i];
170                    let from_out = self.points[i + 1];
171                    let to_in = self.points[i + 2];
172                    let to = self.points[i + 3];
173
174                    i += 3;
175
176                    let index = self.cubic_segments.len();
177                    *ci = NonZeroUsize::new(index + 1).unwrap();
178
179                    let part_length = segment_cubic(
180                        from,
181                        from_out,
182                        to_in,
183                        to,
184                        0.0,
185                        0.0,
186                        1.0,
187                        &mut self.cubic_segments,
188                    );
189                    self.lengths.push(part_length);
190                    length += part_length;
191                    part.num_segments = self.cubic_segments.len() - index;
192                }
193            }
194        }
195
196        length
197    }
198
199    pub fn trimmed(
200        &self,
201        builder: &mut CommandPathBuilder,
202        start_len: f32,
203        end_len: f32,
204        move_to: bool,
205    ) {
206        assert!(end_len >= start_len);
207
208        if start_len == end_len || self.parts.is_empty() {
209            return;
210        }
211
212        let parts_and_lengths = self.lengths.iter().scan(0.0, |len, &part_len| {
213            let old_len = *len;
214            *len += part_len;
215
216            Some((old_len, part_len))
217        });
218
219        let first_part = parts_and_lengths
220            .clone()
221            .enumerate()
222            .find(|(_, (len, part_len))| len + part_len > start_len)
223            .map(|(i, (len, part_len))| (i, (start_len - len) / part_len));
224
225        if let Some((first_part_index, start_t)) = first_part {
226            let (last_part_index, end_t) = parts_and_lengths
227                .enumerate()
228                .skip(first_part_index)
229                .find(|(_, (len, part_len))| len + part_len >= end_len)
230                .map(|(i, (len, part_len))| (i, (end_len - len) / part_len))
231                .unwrap_or_else(|| (self.parts.len() - 1, 1.0));
232
233            let start_t = start_t.clamp(0.0, 1.0);
234            let end_t = end_t.clamp(0.0, 1.0);
235
236            if first_part_index == last_part_index {
237                self.extract_sub_part(first_part_index, start_t, end_t, move_to, builder);
238            } else {
239                self.extract_sub_part(first_part_index, start_t, 1.0, move_to, builder);
240
241                for part in &self.parts[first_part_index + 1..last_part_index] {
242                    match part.r#type {
243                        PathPartType::Line => {
244                            builder.line_to(self.points[part.offset]);
245                        }
246                        PathPartType::Cubic(_) => {
247                            builder.cubic_to(
248                                self.points[part.offset],
249                                self.points[part.offset + 1],
250                                self.points[part.offset + 2],
251                            );
252                        }
253                    }
254                }
255
256                self.extract_sub_part(last_part_index, 0.0, end_t, false, builder);
257            }
258        }
259    }
260
261    fn extract_sub_part(
262        &self,
263        i: usize,
264        mut start_t: f32,
265        mut end_t: f32,
266        move_to: bool,
267        builder: &mut CommandPathBuilder,
268    ) {
269        let part = self.parts[i];
270        match part.r#type {
271            PathPartType::Line => {
272                let from = self.points[part.offset - 1];
273                let to = self.points[part.offset];
274
275                let dir = to - from;
276
277                if move_to {
278                    builder.move_to(from + dir * start_t);
279                }
280                builder.line_to(from + dir * end_t);
281            }
282            PathPartType::Cubic(ci) => {
283                let starting_segment_index = ci.get() - 1;
284                let mut start_end_segment_index = starting_segment_index;
285                let ending_segment_index = starting_segment_index + part.num_segments;
286
287                let len = self.lengths[i];
288                if start_t != 0.0 {
289                    let start_len = start_t * len;
290                    for si in starting_segment_index..ending_segment_index {
291                        let segment = self.cubic_segments[si];
292                        if segment.len >= start_len {
293                            if si == starting_segment_index {
294                                start_t = segment.t * (start_len / segment.len);
295                            } else {
296                                let previous_len = self.cubic_segments[si - 1].len;
297
298                                let t = (start_len - previous_len) / (segment.len - previous_len);
299                                start_t = math::lerp(self.cubic_segments[si - 1].t, segment.t, t);
300                            }
301
302                            // Help out the ending segment finder by setting its
303                            // start to where we landed while finding the first
304                            // segment, that way it can skip a bunch of work.
305                            start_end_segment_index = si;
306                            break;
307                        }
308                    }
309                }
310
311                if end_t != 1.0 {
312                    let end_len = end_t * len;
313                    for si in start_end_segment_index..ending_segment_index {
314                        let segment = self.cubic_segments[si];
315                        if segment.len >= end_len {
316                            if si == starting_segment_index {
317                                end_t = segment.t * (end_len / segment.len);
318                            } else {
319                                let previous_len = self.cubic_segments[si - 1].len;
320
321                                let t = (end_len - previous_len) / (segment.len - previous_len);
322                                end_t = math::lerp(self.cubic_segments[si - 1].t, segment.t, t);
323                            }
324
325                            break;
326                        }
327                    }
328                }
329
330                let mut hull = [math::Vec::default(); 6];
331
332                let from = self.points[part.offset - 1];
333                let from_out = self.points[part.offset];
334                let to_in = self.points[part.offset + 1];
335                let to = self.points[part.offset + 2];
336
337                if start_t == 0.0 {
338                    compute_hull(from, from_out, to_in, to, end_t, &mut hull);
339
340                    if move_to {
341                        builder.move_to(from);
342                    }
343                    builder.cubic_to(hull[0], hull[3], hull[5]);
344                } else {
345                    // Split at start since it's non 0.
346                    compute_hull(from, from_out, to_in, to, start_t, &mut hull);
347
348                    if move_to {
349                        // Move to first point on the right side.
350                        builder.move_to(hull[5]);
351                    }
352                    if end_t == 1.0 {
353                        // End is 1, so no further split is necessary just cubicTo
354                        // the remaining right side.
355                        builder.cubic_to(hull[4], hull[2], to);
356                    } else {
357                        // End is not 1, so split again and cubic to the left side
358                        // of the split and remap endT to the new curve range.
359                        compute_hull(
360                            hull[5],
361                            hull[4],
362                            hull[2],
363                            to,
364                            (end_t - start_t) / (1.0 - start_t),
365                            &mut hull,
366                        );
367
368                        builder.cubic_to(hull[0], hull[3], hull[5]);
369                    }
370                }
371            }
372        }
373    }
374}