1use 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#[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#[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#[derive(Copy, Clone, Debug, PartialEq)]
65pub struct Nearest {
66 pub subpath: usize,
68 pub segment: usize,
70 pub t: f64,
72 pub point: Point,
74 pub distance: f64,
76}
77
78fn 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#[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
138fn 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#[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#[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#[must_use]
204pub fn to_path(geometry: &EvaluatedGeometry, size: CanvasSize) -> EvaluatedPath {
205 from_bez(&geometry.to_path(size), size)
206}
207
208#[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 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 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#[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 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
317fn 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#[must_use]
353pub fn items(items: &[EvaluatedItem], size: CanvasSize) -> Vec<ItemPath> {
354 crate::item_paths(items, size)
355}