Skip to main content

alpha2_geometry/
query.rs

1//! What an editor asks of shapes: where items are, which one is under a
2//! point, how long a path is, and paths cut, resampled, or converted. Each
3//! answer comes from the code the renderer draws with.
4//!
5//! Inputs and outputs are normalized as the model is, on a picture of a
6//! given size; lengths and distances are in its pixels.
7
8use crate::tree::{ItemPath, build_scene_with_items};
9use crate::trim::{self, Measured, TrimSpan};
10use crate::{
11    CanvasSize, EvaluatedGeometry, EvaluatedItem, EvaluatedPath, EvaluatedSubpath, EvaluatedVertex,
12    VectorOp, affine, arclen, raster,
13};
14use alpha2_model::FillRule;
15use kurbo::{Affine, BezPath, ParamCurve, ParamCurveDeriv, PathSeg, Point, Rect, Vec2};
16
17/// The normalized bounds of every item, in document order: geometry's
18/// outline, a paint's area (a stroke's with its width), a group's items'.
19/// Trims are not listed; an item with nothing to show has `None`.
20#[must_use]
21pub fn shape_bounds(items: &[EvaluatedItem], size: CanvasSize) -> Vec<(String, Option<Rect>)> {
22    crate::item_paths(items, size)
23        .into_iter()
24        .map(|record| {
25            let empty = record.bounds.width() <= 0.0 && record.bounds.height() <= 0.0;
26            let bounds = (!empty).then(|| normalized_rect(record.bounds, size));
27            (record.at, bounds)
28        })
29        .collect()
30}
31
32fn normalized_rect(rect: Rect, size: CanvasSize) -> Rect {
33    let scale = size.scale();
34    Rect::new(
35        rect.x0 / scale.x,
36        rect.y0 / scale.y,
37        rect.x1 / scale.x,
38        rect.y1 / scale.y,
39    )
40}
41
42/// The front-most fill or stroke that paints the normalized `point`, by
43/// its fill rule (a stroke by its outline): the item's place in the tree.
44#[must_use]
45pub fn hit_test(items: &[EvaluatedItem], size: CanvasSize, point: Point) -> Option<String> {
46    let device = size.point(point.x, point.y);
47    let (scene, owners) = build_scene_with_items(items, size);
48    for (op, owner) in scene.ops.iter().zip(owners).rev() {
49        if let VectorOp::Fill { path, rule, .. } = op {
50            let winding = raster::winding_at(path, device);
51            let inside = match rule {
52                FillRule::Nonzero => winding != 0,
53                FillRule::Evenodd => winding % 2 != 0,
54            };
55            if inside {
56                return Some(owner);
57            }
58        }
59    }
60    None
61}
62
63/// The point of a path nearest another.
64#[derive(Copy, Clone, Debug, PartialEq)]
65pub struct Nearest {
66    /// The subpath.
67    pub subpath: usize,
68    /// The segment within it: segment `i` runs from vertex `i` to the next.
69    pub segment: usize,
70    /// The parameter along the segment, in `0..=1`.
71    pub t: f64,
72    /// The nearest point, normalized.
73    pub point: Point,
74    /// Its distance from the point asked about, in pixels.
75    pub distance: f64,
76}
77
78/// The pixel subpaths of `path` on a picture of `size`, closed ones with
79/// their closing segment.
80fn pixel_subpaths(path: &EvaluatedPath, size: CanvasSize) -> Vec<trim::Subpath> {
81    path.subpaths
82        .iter()
83        .map(|subpath| {
84            let one = EvaluatedPath {
85                subpaths: vec![subpath.clone()],
86            };
87            let bez = EvaluatedGeometry::Path(one.into()).to_path(size);
88            trim::subpaths(&bez)
89                .into_iter()
90                .next()
91                .unwrap_or(trim::Subpath {
92                    segments: Vec::new(),
93                    closed: subpath.closed,
94                    start: subpath.vertices.first().map_or(Point::ZERO, |vertex| {
95                        size.point(vertex.point.x, vertex.point.y)
96                    }),
97                })
98        })
99        .collect()
100}
101
102/// The point of `path` nearest the normalized `point`, measured in pixels.
103#[must_use]
104pub fn nearest_point(path: &EvaluatedPath, size: CanvasSize, point: Point) -> Option<Nearest> {
105    let device = size.point(point.x, point.y);
106    let scale = size.scale();
107    let mut best: Option<Nearest> = None;
108    for (index, subpath) in pixel_subpaths(path, size).iter().enumerate() {
109        if subpath.segments.is_empty() {
110            let distance = affine::distance(subpath.start, device);
111            if best.is_none_or(|best| distance < best.distance) {
112                best = Some(Nearest {
113                    subpath: index,
114                    segment: 0,
115                    t: 0.0,
116                    point: Point::new(subpath.start.x / scale.x, subpath.start.y / scale.y),
117                    distance,
118                });
119            }
120        }
121        for (segment_index, segment) in subpath.segments.iter().enumerate() {
122            let (t, distance) = nearest_on_segment(segment, device);
123            if best.is_none_or(|best| distance < best.distance) {
124                let at = segment.eval(t);
125                best = Some(Nearest {
126                    subpath: index,
127                    segment: segment_index,
128                    t,
129                    point: Point::new(at.x / scale.x, at.y / scale.y),
130                    distance,
131                });
132            }
133        }
134    }
135    best
136}
137
138/// The parameter of `segment` nearest `point`, and the distance: the best
139/// of 64 samples, refined by ternary search around it.
140fn nearest_on_segment(segment: &PathSeg, point: Point) -> (f64, f64) {
141    const SAMPLES: u32 = 64;
142    let distance = |t: f64| affine::distance(segment.eval(t), point);
143    let mut best = (0.0, distance(0.0));
144    for step in 1..=SAMPLES {
145        let t = f64::from(step) / f64::from(SAMPLES);
146        let d = distance(t);
147        if d < best.1 {
148            best = (t, d);
149        }
150    }
151    let span = 1.0 / f64::from(SAMPLES);
152    let (mut low, mut high) = ((best.0 - span).max(0.0), (best.0 + span).min(1.0));
153    for _ in 0..40 {
154        let third = (high - low) / 3.0;
155        let (a, b) = (low + third, high - third);
156        if distance(a) < distance(b) {
157            high = b;
158        } else {
159            low = a;
160        }
161    }
162    let t = f64::midpoint(low, high);
163    let d = distance(t);
164    if d < best.1 { (t, d) } else { best }
165}
166
167/// Each subpath's length in pixels.
168#[must_use]
169pub fn path_lengths(path: &EvaluatedPath, size: CanvasSize) -> Vec<f64> {
170    pixel_subpaths(path, size)
171        .iter()
172        .map(|subpath| trim::subpath_length(subpath, Affine::IDENTITY))
173        .collect()
174}
175
176/// `path` trimmed as a trim item trims it, together: the kept span of its
177/// subpaths' lengths in pixels, normalized again.
178#[must_use]
179pub fn trim_path(
180    path: &EvaluatedPath,
181    size: CanvasSize,
182    start: f64,
183    end: f64,
184    offset: f64,
185) -> EvaluatedPath {
186    let parts: Vec<Measured> = pixel_subpaths(path, size)
187        .into_iter()
188        .map(|subpath| Measured {
189            subpath,
190            measure: Affine::IDENTITY,
191        })
192        .collect();
193    let kept = trim::trim_together(&parts, TrimSpan { start, end, offset });
194    let mut bez = BezPath::new();
195    for (_, subpath) in &kept {
196        trim::append(&mut bez, subpath);
197    }
198    from_bez(&bez, size)
199}
200
201/// `geometry` as a path of the same outline, normalized: the editor's
202/// "convert to path".
203#[must_use]
204pub fn to_path(geometry: &EvaluatedGeometry, size: CanvasSize) -> EvaluatedPath {
205    from_bez(&geometry.to_path(size), size)
206}
207
208/// A pixel path as vertices and tangents, normalized. Quadratic segments
209/// are raised to cubics; a closed subpath's closing line back to its start
210/// is left implicit.
211#[must_use]
212pub fn from_bez(path: &BezPath, size: CanvasSize) -> EvaluatedPath {
213    let scale = size.scale();
214    let point = |p: Point| Point::new(p.x / scale.x, p.y / scale.y);
215    let vector = |v: Vec2| Vec2::new(v.x / scale.x, v.y / scale.y);
216    let subpaths = trim::subpaths(path)
217        .into_iter()
218        .map(|subpath| {
219            let mut segments = subpath.segments.clone();
220            // The closing segment, when a straight line, is the implicit one.
221            if subpath.closed
222                && let Some(PathSeg::Line(_)) = segments.last()
223                && segments.len() > 1
224            {
225                segments.pop();
226            }
227            let mut vertices = vec![EvaluatedVertex {
228                point: point(subpath.start),
229                ..EvaluatedVertex::default()
230            }];
231            for segment in &segments {
232                let cubic = match *segment {
233                    PathSeg::Line(line) => {
234                        vertices.push(EvaluatedVertex {
235                            point: point(line.p1),
236                            ..EvaluatedVertex::default()
237                        });
238                        continue;
239                    }
240                    PathSeg::Quad(quad) => quad.raise(),
241                    PathSeg::Cubic(cubic) => cubic,
242                };
243                if let Some(last) = vertices.last_mut() {
244                    last.out_tangent = vector(cubic.p1 - cubic.p0);
245                }
246                vertices.push(EvaluatedVertex {
247                    point: point(cubic.p3),
248                    in_tangent: vector(cubic.p2 - cubic.p3),
249                    out_tangent: Vec2::ZERO,
250                });
251            }
252            // A closed subpath's last segment ends on its first vertex: fold it in.
253            if subpath.closed
254                && vertices.len() > 1
255                && let Some(last) = vertices.pop()
256            {
257                if last.point == vertices[0].point {
258                    vertices[0].in_tangent = last.in_tangent;
259                } else {
260                    vertices.push(last);
261                }
262            }
263            EvaluatedSubpath {
264                closed: subpath.closed,
265                vertices,
266            }
267        })
268        .collect();
269    EvaluatedPath { subpaths }
270}
271
272/// `path` with `counts[i]` vertices on subpath `i`, spaced evenly along its
273/// pixel length, with tangents along the curve a third of the spacing long:
274/// paths of different vertex counts made ready to morph. A subpath with no
275/// count, or a count under 2 (3 when closed), is left as it is.
276#[must_use]
277pub fn resample_path(path: &EvaluatedPath, size: CanvasSize, counts: &[usize]) -> EvaluatedPath {
278    let scale = size.scale();
279    let subpaths = pixel_subpaths(path, size)
280        .into_iter()
281        .zip(&path.subpaths)
282        .enumerate()
283        .map(|(index, (pixels, original))| {
284            let count = counts.get(index).copied().unwrap_or(0);
285            let minimum = if original.closed { 3 } else { 2 };
286            let total = trim::subpath_length(&pixels, Affine::IDENTITY);
287            if count < minimum || total <= 0.0 {
288                return original.clone();
289            }
290            // A closed subpath closes back to its first vertex, so its `count` vertices split it
291            // into `count` spans; an open one's into `count - 1`.
292            let spans = if original.closed { count } else { count - 1 };
293            #[allow(clippy::cast_precision_loss)]
294            let spacing = total / spans as f64;
295            let vertices = (0..count)
296                .map(|step| {
297                    #[allow(clippy::cast_precision_loss)]
298                    let distance = spacing * step as f64;
299                    let (at, direction) = point_and_direction(&pixels, distance);
300                    let handle = direction * (spacing / 3.0);
301                    EvaluatedVertex {
302                        point: Point::new(at.x / scale.x, at.y / scale.y),
303                        in_tangent: Vec2::new(-handle.x / scale.x, -handle.y / scale.y),
304                        out_tangent: Vec2::new(handle.x / scale.x, handle.y / scale.y),
305                    }
306                })
307                .collect();
308            EvaluatedSubpath {
309                closed: original.closed,
310                vertices,
311            }
312        })
313        .collect();
314    EvaluatedPath { subpaths }
315}
316
317/// The point `distance` along `subpath` and the unit direction of travel
318/// there.
319fn point_and_direction(subpath: &trim::Subpath, distance: f64) -> (Point, Vec2) {
320    let mut run = 0.0;
321    for segment in &subpath.segments {
322        let length = arclen::length(segment);
323        if distance <= run + length && length > 0.0 {
324            let t = arclen::parameter_at(segment, distance - run, length);
325            return (segment.eval(t), direction(segment, t));
326        }
327        run += length;
328    }
329    subpath
330        .segments
331        .last()
332        .map_or((subpath.start, Vec2::ZERO), |segment| {
333            (segment.end(), direction(segment, 1.0))
334        })
335}
336
337fn direction(segment: &PathSeg, t: f64) -> Vec2 {
338    let derivative = match segment {
339        PathSeg::Line(line) => line.p1 - line.p0,
340        PathSeg::Quad(quad) => quad.deriv().eval(t).to_vec2(),
341        PathSeg::Cubic(cubic) => cubic.deriv().eval(t).to_vec2(),
342    };
343    let length = affine::length(derivative);
344    if length > 0.0 {
345        derivative / length
346    } else {
347        Vec2::ZERO
348    }
349}
350
351/// Every item's [`ItemPath`] (place in the tree, device path, and bounds), in document order.
352#[must_use]
353pub fn items(items: &[EvaluatedItem], size: CanvasSize) -> Vec<ItemPath> {
354    crate::item_paths(items, size)
355}