1use 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#[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#[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#[derive(Copy, Clone, Debug, PartialEq)]
61pub struct Nearest {
62 pub subpath: usize,
64 pub segment: usize,
66 pub t: f64,
68 pub point: Point,
70 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#[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
132fn 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#[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#[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#[must_use]
196pub fn to_path(geometry: &EvaluatedGeometry, size: CanvasSize) -> EvaluatedPath {
197 from_bez(&geometry.to_path(size), size)
198}
199
200#[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 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 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#[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 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
306fn 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#[must_use]
341pub fn items(items: &[EvaluatedItem], size: CanvasSize) -> Vec<ItemPath> {
342 crate::item_paths(items, size)
343}