Skip to main content

alpha2_geometry/
query.rs

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