1use super::super::{PointCoord, PointFlags};
4use crate::{model::glyph::outline::PathSink, types::Point};
5
6#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
12pub enum PathContourStart {
13 #[default]
18 ScanBackward,
19 ScanForward,
24}
25
26#[must_use]
40pub fn outline_to_path<C: PointCoord>(
41 points: &[Point<C>],
42 flags: &[PointFlags],
43 contours: &[u16],
44 start: PathContourStart,
45 sink: &mut impl PathSink,
46) -> bool {
47 if flags.len() != points.len() {
48 return false;
49 }
50 let mut base = 0usize;
51 for end in contours {
52 let end = *end as usize;
53 if end < base || end >= points.len() {
54 return false;
55 }
56 let (points, flags) = (&points[base..=end], &flags[base..=end]);
57 if !walk_contour(
58 points.len(),
59 |ix| ContourPoint::new(points[ix], flags[ix]),
60 start,
61 sink,
62 ) {
63 return false;
64 }
65 base = end + 1;
66 }
67 true
68}
69
70#[must_use]
112pub fn contour_to_path<T, C: PointCoord>(
113 points: &[T],
114 to_point: impl Fn(&T) -> (Point<C>, PointFlags),
115 start: PathContourStart,
116 sink: &mut impl PathSink,
117) -> bool {
118 walk_contour(
119 points.len(),
120 |ix| {
121 let (point, flags) = to_point(&points[ix]);
122 ContourPoint::new(point, flags)
123 },
124 start,
125 sink,
126 )
127}
128
129fn walk_contour<C: PointCoord>(
136 len: usize,
137 point: impl Fn(usize) -> ContourPoint<C>,
138 start: PathContourStart,
139 sink: &mut impl PathSink,
140) -> bool {
141 if len == 0 {
142 return true;
143 }
144 let first = point(0);
145 let (start_point, base, count) = if first.flags.is_on_curve() {
148 (first, 1, len - 1)
150 } else if first.flags.is_off_curve_cubic() {
151 let last = point(len - 1);
152 if last.flags.is_on_curve() {
153 (last, 0, len - 1)
154 } else if last.flags.is_off_curve_cubic() {
155 (last.midpoint(first), 0, len)
158 } else {
159 return false;
160 }
161 } else {
162 match start {
163 PathContourStart::ScanBackward => {
164 let last = point(len - 1);
165 if last.flags.is_on_curve() {
166 (last, 0, len - 1)
168 } else if last.flags.is_off_curve_quad() {
169 (last.midpoint(first), 0, len)
170 } else {
171 return false;
172 }
173 }
174 PathContourStart::ScanForward => {
175 if len < 2 {
176 return true;
178 }
179 let second = point(1);
180 if second.flags.is_on_curve() {
181 (second, 2, len)
182 } else if second.flags.is_off_curve_quad() {
183 (first.midpoint(second), 1, len)
184 } else {
185 return false;
186 }
187 }
188 }
189 };
190 let start_f32 = start_point.point_f32();
191 sink.move_to(start_f32.x, start_f32.y);
192 let mut state = PendingState::default();
193 let wrapped = (base + count).saturating_sub(len);
196 for ix in base..base + count - wrapped {
197 if !state.emit(point(ix), sink) {
198 return false;
199 }
200 }
201 for ix in 0..wrapped {
202 if !state.emit(point(ix), sink) {
203 return false;
204 }
205 }
206 state.finish(start_point, sink)
207}
208
209#[derive(Copy, Clone, Default, Debug)]
211struct ContourPoint<C> {
212 x: C,
213 y: C,
214 flags: PointFlags,
215}
216
217impl<C: PointCoord> ContourPoint<C> {
218 fn new(point: Point<C>, flags: PointFlags) -> Self {
219 Self {
220 x: point.x,
221 y: point.y,
222 flags,
223 }
224 }
225
226 fn point_f32(&self) -> Point<f32> {
227 Point::new(self.x.to_f32(), self.y.to_f32())
228 }
229
230 fn midpoint(&self, other: Self) -> Self {
231 Self {
232 x: self.x.midpoint(other.x),
233 y: self.y.midpoint(other.y),
234 flags: other.flags,
235 }
236 }
237}
238
239#[derive(Copy, Clone, Default)]
242enum PendingState<C> {
243 #[default]
244 Empty,
245 Quad(ContourPoint<C>),
246 Cubic(ContourPoint<C>),
247 TwoCubics(ContourPoint<C>, ContourPoint<C>),
248}
249
250impl<C: PointCoord> PendingState<C> {
251 #[inline(always)]
252 fn emit(&mut self, point: ContourPoint<C>, sink: &mut impl PathSink) -> bool {
253 let flags = point.flags;
254 match *self {
255 Self::Empty => {
256 if flags.is_off_curve_quad() {
257 *self = Self::Quad(point);
258 } else if flags.is_off_curve_cubic() {
259 *self = Self::Cubic(point);
260 } else {
261 let p = point.point_f32();
262 sink.line_to(p.x, p.y);
263 }
264 }
265 Self::Quad(quad) => {
266 if flags.is_off_curve_quad() {
267 let c0 = quad.point_f32();
269 let p = quad.midpoint(point).point_f32();
270 sink.quad_to(c0.x, c0.y, p.x, p.y);
271 *self = Self::Quad(point);
272 } else if flags.is_off_curve_cubic() {
273 return false;
274 } else {
275 let c0 = quad.point_f32();
276 let p = point.point_f32();
277 sink.quad_to(c0.x, c0.y, p.x, p.y);
278 *self = Self::Empty;
279 }
280 }
281 Self::Cubic(cubic) => {
282 if flags.is_off_curve_cubic() {
283 *self = Self::TwoCubics(cubic, point);
284 } else {
285 return false;
286 }
287 }
288 Self::TwoCubics(cubic0, cubic1) => {
289 if flags.is_off_curve_quad() {
290 return false;
291 } else if flags.is_off_curve_cubic() {
292 let c0 = cubic0.point_f32();
294 let c1 = cubic1.point_f32();
295 let p = cubic1.midpoint(point).point_f32();
296 sink.curve_to(c0.x, c0.y, c1.x, c1.y, p.x, p.y);
297 *self = Self::Cubic(point);
298 } else {
299 let c0 = cubic0.point_f32();
300 let c1 = cubic1.point_f32();
301 let p = point.point_f32();
302 sink.curve_to(c0.x, c0.y, c1.x, c1.y, p.x, p.y);
303 *self = Self::Empty;
304 }
305 }
306 }
307 true
308 }
309
310 fn finish(mut self, mut start_point: ContourPoint<C>, sink: &mut impl PathSink) -> bool {
311 if !matches!(self, Self::Empty) {
312 start_point.flags = PointFlags::on_curve();
314 if !self.emit(start_point, sink) {
315 return false;
316 }
317 }
318 sink.close();
319 true
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::model::glyph::outline::SvgSink;
327 use crate::types::F26Dot6;
328 use alloc::vec::Vec;
329
330 fn contour(points: &[(f32, f32, char)]) -> (Vec<Point<f32>>, Vec<PointFlags>) {
333 points
334 .iter()
335 .map(|(x, y, kind)| {
336 let flags = match kind {
337 'q' => PointFlags::off_curve_quad(),
338 'c' => PointFlags::off_curve_cubic(),
339 _ => PointFlags::on_curve(),
340 };
341 (Point::new(*x, *y), flags)
342 })
343 .unzip()
344 }
345
346 fn draw(
347 points: &[(f32, f32, char)],
348 contours: &[u16],
349 start: PathContourStart,
350 ) -> Option<String> {
351 let (points, flags) = contour(points);
352 let mut pen = SvgSink::with_precision(1);
353 outline_to_path(&points, &flags, contours, start, &mut pen)
354 .then(|| pen.as_ref().to_string())
355 }
356
357 fn draw_off_curve(expected: &str, start: PathContourStart, all_off_curve: bool) {
360 fn pt(x: i32, y: i32) -> Point<F26Dot6> {
361 Point::new(x, y).map(F26Dot6::from_bits)
362 }
363 let mut flags = [PointFlags::off_curve_quad(); 4];
364 if !all_off_curve {
365 flags[1] = PointFlags::on_curve();
366 }
367 let points = [pt(640, 128), pt(256, 64), pt(640, 64), pt(128, 128)];
368 let mut pen = SvgSink::with_precision(1);
369 assert!(outline_to_path(&points, &flags, &[3], start, &mut pen));
370 assert_eq!(pen.as_ref(), expected);
371 }
372
373 #[test]
378 fn a_contour_of_off_curve_points_starts_between_its_ends_scanning_backward() {
379 draw_off_curve(
380 "M6.0,2.0 Q10.0,2.0 7.0,1.5 Q4.0,1.0 7.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Z",
381 PathContourStart::ScanBackward,
382 true,
383 );
384 }
385
386 #[test]
387 fn a_contour_of_off_curve_points_starts_between_its_ends_scanning_forward() {
388 draw_off_curve(
389 "M7.0,1.5 Q4.0,1.0 7.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Q10.0,2.0 7.0,1.5 Z",
390 PathContourStart::ScanForward,
391 true,
392 );
393 }
394
395 #[test]
396 fn a_contour_beginning_off_curve_scans_backward_to_an_on_curve_point() {
397 draw_off_curve(
398 "M6.0,2.0 Q10.0,2.0 4.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Z",
399 PathContourStart::ScanBackward,
400 false,
401 );
402 }
403
404 #[test]
405 fn a_contour_beginning_off_curve_scans_forward_to_an_on_curve_point() {
406 draw_off_curve(
407 "M4.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Q10.0,2.0 4.0,1.0 Z",
408 PathContourStart::ScanForward,
409 false,
410 );
411 }
412
413 #[test]
414 fn a_contour_of_on_curve_points_is_a_run_of_lines() {
415 assert_eq!(
416 draw(
417 &[(0.0, 0.0, 'n'), (8.0, 0.0, 'n'), (8.0, 8.0, 'n')],
418 &[2],
419 PathContourStart::ScanBackward
420 )
421 .unwrap(),
422 "M0.0,0.0 L8.0,0.0 L8.0,8.0 Z"
423 );
424 }
425
426 #[test]
427 fn a_contour_beginning_off_curve_starts_where_the_caller_asks() {
428 let points = [(0.0, 0.0, 'q'), (8.0, 0.0, 'n'), (8.0, 8.0, 'n')];
431 let backward = draw(&points, &[2], PathContourStart::ScanBackward).unwrap();
432 let forward = draw(&points, &[2], PathContourStart::ScanForward).unwrap();
433 assert!(backward.starts_with("M8.0,8.0"), "{backward}");
434 assert!(forward.starts_with("M8.0,0.0"), "{forward}");
435 assert_ne!(backward, forward);
436 }
437
438 #[test]
439 fn two_contours_are_drawn_in_turn() {
440 let drawn = draw(
441 &[
442 (0.0, 0.0, 'n'),
443 (4.0, 0.0, 'n'),
444 (8.0, 8.0, 'n'),
445 (9.0, 9.0, 'n'),
446 ],
447 &[1, 3],
448 PathContourStart::ScanBackward,
449 )
450 .unwrap();
451 assert_eq!(drawn, "M0.0,0.0 L4.0,0.0 Z M8.0,8.0 L9.0,9.0 Z");
452 }
453
454 #[test]
455 fn points_that_do_not_describe_a_path_are_refused() {
456 assert_eq!(
459 draw(
460 &[(0.0, 0.0, 'n'), (4.0, 4.0, 'c'), (8.0, 0.0, 'n')],
461 &[2],
462 PathContourStart::ScanBackward
463 ),
464 None
465 );
466 assert_eq!(
467 draw(
468 &[(0.0, 0.0, 'n'), (8.0, 0.0, 'n')],
469 &[7],
470 PathContourStart::ScanBackward
471 ),
472 None
473 );
474 }
475
476 #[test]
477 fn a_flag_per_point_is_required() {
478 let (points, _) = contour(&[(0.0, 0.0, 'n'), (8.0, 0.0, 'n')]);
479 let mut pen = SvgSink::with_precision(1);
480 assert!(!outline_to_path(
481 &points,
482 &[PointFlags::on_curve()],
483 &[1],
484 PathContourStart::ScanBackward,
485 &mut pen
486 ));
487 }
488
489 #[test]
490 fn cubic_contours_can_start_with_controls() {
491 for start in [
492 PathContourStart::ScanBackward,
493 PathContourStart::ScanForward,
494 ] {
495 assert_eq!(
496 draw(
497 &[(0.0, 0.0, 'c'), (80.0, 0.0, 'c'), (80.0, 80.0, 'n')],
498 &[2],
499 start,
500 )
501 .unwrap(),
502 "M80.0,80.0 C0.0,0.0 80.0,0.0 80.0,80.0 Z"
503 );
504 assert_eq!(
505 draw(
506 &[
507 (0.0, 0.0, 'c'),
508 (80.0, 0.0, 'c'),
509 (80.0, 80.0, 'c'),
510 (0.0, 80.0, 'c'),
511 ],
512 &[3],
513 start,
514 )
515 .unwrap(),
516 "M0.0,40.0 C0.0,0.0 80.0,0.0 80.0,40.0 C80.0,80.0 0.0,80.0 0.0,40.0 Z"
517 );
518 }
519 }
520
521 #[test]
522 fn cubic_flag_on_on_curve_points_is_allowed() {
523 let (points, mut flags) = contour(&[(0.0, 0.0, 'n'), (10.0, 0.0, 'n'), (10.0, 10.0, 'n')]);
524 flags[0] = PointFlags::from_bits(0x81);
525 flags[1] = PointFlags::from_bits(0x81);
526 assert!(!flags[0].is_off_curve());
527 let mut pen = SvgSink::with_precision(1);
528 assert!(outline_to_path(
529 &points,
530 &flags,
531 &[2],
532 PathContourStart::ScanBackward,
533 &mut pen
534 ));
535 assert_eq!(pen.as_ref(), "M0.0,0.0 L10.0,0.0 L10.0,10.0 Z");
536 flags[0].flip_on_curve();
537 assert!(flags[0].is_off_curve_cubic());
538 }
539
540 #[test]
541 fn invalid_cubic_runs_are_refused() {
542 for kinds in ["ccc", "cnc", "ccqn", "qccn", "ccnc"] {
543 let points: Vec<_> = kinds.chars().map(|kind| (0.0, 0.0, kind)).collect();
544 for start in [
545 PathContourStart::ScanBackward,
546 PathContourStart::ScanForward,
547 ] {
548 assert_eq!(
549 draw(&points, &[(points.len() - 1) as u16], start),
550 None,
551 "{kinds}: {start:?}"
552 );
553 }
554 }
555 }
556}