Skip to main content

gpui_ce_motion/vector/
tools.rs

1//! Prepared arc-length queries and bounded contour resampling.
2use super::{PathCommand, PathError, PathShape, flatten, lerp};
3
4#[derive(Clone, Copy, Debug)]
5struct Segment {
6    from: [f64; 2],
7    to: [f64; 2],
8    end: f64,
9}
10
11/// A position and forward tangent on a prepared path.
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct PathPoint {
14    /// Logical-pixel position, without any native element transform.
15    pub position: [f64; 2],
16    /// Tangent heading in degrees, useful for SVG drawing rotation.
17    pub tangent_degrees: f64,
18}
19
20/// Immutable arc-length lookup. Curves use the engine's 32-step approximation.
21/// Separate subpaths do not introduce connecting segments. Setup allocates;
22/// repeated queries use binary search and allocate no memory.
23#[derive(Clone, Debug)]
24pub struct PathMeasure {
25    segments: Vec<Segment>,
26    length: f64,
27}
28
29impl PathMeasure {
30    /// Prepares a non-degenerate path. Empty/zero-length geometry is rejected.
31    pub fn new(shape: &PathShape) -> Result<Self, PathError> {
32        Self::from_commands(shape.commands())
33    }
34
35    fn from_commands(commands: &[PathCommand]) -> Result<Self, PathError> {
36        let mut vertices = Vec::new();
37
38        flatten(commands, &mut vertices);
39        let mut segments = Vec::new();
40        let mut length = 0.0;
41
42        for pair in vertices.windows(2).filter(|pair| !pair[1].start) {
43            let from = pair[0].point;
44            let to = pair[1].point;
45            let distance = (to[0] - from[0]).hypot(to[1] - from[1]);
46
47            if distance > 0.0 {
48                length += distance;
49                segments.push(Segment {
50                    from,
51                    to,
52                    end: length,
53                });
54            }
55        }
56
57        if segments.is_empty() {
58            return Err(PathError::Geometry);
59        }
60
61        Ok(Self { segments, length })
62    }
63
64    /// Approximate total length in logical pixels.
65    pub fn length(&self) -> f64 {
66        self.length
67    }
68
69    /// Samples normalized length, clamped to `0..=1`. Panics for non-finite input.
70    /// A subpath boundary chooses the preceding endpoint; the next positive step
71    /// enters the next subpath without interpolating across the gap.
72    pub fn point(&self, progress: f64) -> PathPoint {
73        assert!(progress.is_finite(), "path progress must be finite");
74        let distance = progress.clamp(0.0, 1.0) * self.length;
75
76        let index = self
77            .segments
78            .partition_point(|segment| segment.end < distance)
79            .min(self.segments.len() - 1);
80
81        let segment = self.segments[index];
82
83        let start = if index == 0 {
84            0.0
85        } else {
86            self.segments[index - 1].end
87        };
88
89        let t = ((distance - start) / (segment.end - start)).clamp(0.0, 1.0);
90
91        PathPoint {
92            position: lerp(segment.from, segment.to, t),
93            tangent_degrees: (segment.to[1] - segment.from[1])
94                .atan2(segment.to[0] - segment.from[0])
95                .to_degrees(),
96        }
97    }
98}
99
100impl PathShape {
101    /// Prepares equally sampled line topology for two paths. Subpaths match by
102    /// order and must agree on open/closed status; winding and start points are
103    /// preserved. This is geometric resampling, not semantic contour matching.
104    /// Closed contours use `samples` points; open ones include both endpoints.
105    /// Curves retain the measurement's documented approximation. Cache the result
106    /// before constructing `PathMorphState` so no conversion happens per frame.
107    ///
108    /// # Errors
109    /// Returns `Topology` for different contour counts/closure, `Geometry` for
110    /// zero-length contours, or `Complexity` outside 2..=4096 samples or above
111    /// 65,536 total output points per shape.
112    pub fn normalize_for_morph(
113        &self,
114        other: &Self,
115        samples: usize,
116    ) -> Result<(Self, Self), PathError> {
117        if !(2..=4096).contains(&samples) {
118            return Err(PathError::Complexity);
119        }
120
121        let contours = |shape: &Self| -> Result<Vec<(PathMeasure, bool)>, PathError> {
122            let commands = shape.commands();
123            let mut result = Vec::new();
124            let mut start = 0;
125
126            for end in 1..=commands.len() {
127                if end == commands.len() || matches!(commands[end], PathCommand::Move(_)) {
128                    result.push((
129                        PathMeasure::from_commands(&commands[start..end])?,
130                        matches!(commands[end - 1], PathCommand::Close),
131                    ));
132                    start = end;
133                }
134            }
135
136            Ok(result)
137        };
138
139        let a = contours(self)?;
140        let b = contours(other)?;
141
142        if a.len() != b.len() || a.iter().zip(&b).any(|(a, b)| a.1 != b.1) {
143            return Err(PathError::Topology);
144        }
145
146        if a.len().saturating_mul(samples) > 65_536 {
147            return Err(PathError::Complexity);
148        }
149
150        let resample = |contours: Vec<(PathMeasure, bool)>| {
151            let mut commands = Vec::new();
152
153            for (measure, closed) in contours {
154                for i in 0..samples {
155                    let divisor = if closed { samples } else { samples - 1 };
156                    let point = measure.point(i as f64 / divisor as f64).position;
157
158                    commands.push(if i == 0 {
159                        PathCommand::Move(point)
160                    } else {
161                        PathCommand::Line(point)
162                    });
163                }
164
165                if closed {
166                    commands.push(PathCommand::Close);
167                }
168            }
169
170            Self::new(commands)
171        };
172
173        Ok((resample(a)?, resample(b)?))
174    }
175}