1use glam::Vec2;
8use std::collections::HashMap;
9use ab_glyph::{Font, FontVec, PxScale, ScaleFont, GlyphId};
10
11#[derive(Clone, Copy, Debug)]
15pub struct GlyphBounds {
16 pub min: Vec2,
17 pub max: Vec2,
18}
19
20impl GlyphBounds {
21 pub fn size(&self) -> Vec2 { self.max - self.min }
22 pub fn center(&self) -> Vec2 { (self.min + self.max) * 0.5 }
23
24 pub fn from_points(points: &[Vec2]) -> Self {
25 let mut min = Vec2::splat(f32::MAX);
26 let mut max = Vec2::splat(f32::MIN);
27 for p in points {
28 min = min.min(*p);
29 max = max.max(*p);
30 }
31 Self { min, max }
32 }
33}
34
35#[derive(Clone, Debug)]
37pub struct Contour {
38 pub points: Vec<Vec2>,
39 pub is_hole: bool,
40}
41
42impl Contour {
43 pub fn signed_area(&self) -> f32 {
44 signed_area(&self.points)
45 }
46
47 pub fn bounds(&self) -> GlyphBounds {
48 GlyphBounds::from_points(&self.points)
49 }
50}
51
52#[derive(Clone, Debug)]
54pub struct GlyphOutline {
55 pub contours: Vec<Contour>,
56 pub advance_width: f32,
57 pub bounds: GlyphBounds,
58}
59
60pub fn signed_area(points: &[Vec2]) -> f32 {
64 let n = points.len();
65 if n < 3 { return 0.0; }
66 let mut area = 0.0f32;
67 for i in 0..n {
68 let j = (i + 1) % n;
69 area += points[i].x * points[j].y;
70 area -= points[j].x * points[i].y;
71 }
72 area * 0.5
73}
74
75pub fn is_ccw(points: &[Vec2]) -> bool {
77 signed_area(points) > 0.0
78}
79
80pub fn point_in_polygon(point: Vec2, polygon: &[Vec2]) -> bool {
84 let n = polygon.len();
85 if n < 3 { return false; }
86 let mut inside = false;
87 let mut j = n - 1;
88 for i in 0..n {
89 let pi = polygon[i];
90 let pj = polygon[j];
91 if ((pi.y > point.y) != (pj.y > point.y))
92 && (point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x)
93 {
94 inside = !inside;
95 }
96 j = i;
97 }
98 inside
99}
100
101pub fn subdivide_quadratic(p0: Vec2, p1: Vec2, p2: Vec2, tolerance: f32) -> Vec<Vec2> {
105 let mut result = Vec::new();
106 subdivide_quad_recursive(p0, p1, p2, tolerance * tolerance, &mut result);
107 result.push(p2);
108 result
109}
110
111fn subdivide_quad_recursive(p0: Vec2, p1: Vec2, p2: Vec2, tol_sq: f32, out: &mut Vec<Vec2>) {
112 let mid = (p0 + p2) * 0.5;
113 let dist_sq = (p1 - mid).length_squared();
114 if dist_sq <= tol_sq {
115 out.push(p0);
116 return;
117 }
118 let q0 = (p0 + p1) * 0.5;
119 let q1 = (p1 + p2) * 0.5;
120 let r = (q0 + q1) * 0.5;
121 subdivide_quad_recursive(p0, q0, r, tol_sq, out);
122 subdivide_quad_recursive(r, q1, p2, tol_sq, out);
123}
124
125pub fn subdivide_cubic(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, tolerance: f32) -> Vec<Vec2> {
127 let mut result = Vec::new();
128 subdivide_cubic_recursive(p0, p1, p2, p3, tolerance * tolerance, 0, &mut result);
129 result.push(p3);
130 result
131}
132
133fn subdivide_cubic_recursive(
134 p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2,
135 tol_sq: f32, depth: u32, out: &mut Vec<Vec2>,
136) {
137 if depth > 10 {
138 out.push(p0);
139 return;
140 }
141 let d1 = (p1 - (p0 + p3) * 0.5).length_squared();
142 let d2 = (p2 - (p0 + p3) * 0.5).length_squared();
143 if d1 + d2 <= tol_sq {
144 out.push(p0);
145 return;
146 }
147 let ab = (p0 + p1) * 0.5;
148 let bc = (p1 + p2) * 0.5;
149 let cd = (p2 + p3) * 0.5;
150 let abc = (ab + bc) * 0.5;
151 let bcd = (bc + cd) * 0.5;
152 let abcd = (abc + bcd) * 0.5;
153 subdivide_cubic_recursive(p0, ab, abc, abcd, tol_sq, depth + 1, out);
154 subdivide_cubic_recursive(abcd, bcd, cd, p3, tol_sq, depth + 1, out);
155}
156
157pub fn classify_contours(contours: &mut [Contour]) {
163 for c in contours.iter_mut() {
164 let area = signed_area(&c.points);
165 if area < 0.0 {
168 c.points.reverse();
170 c.is_hole = false;
171 } else {
172 c.points.reverse();
174 c.is_hole = true;
175 }
176 }
177 let all_holes = contours.iter().all(|c| c.is_hole);
179 if all_holes && !contours.is_empty() {
180 let mut max_area = 0.0f32;
181 let mut max_idx = 0;
182 for (i, c) in contours.iter().enumerate() {
183 let a = signed_area(&c.points).abs();
184 if a > max_area {
185 max_area = a;
186 max_idx = i;
187 }
188 }
189 contours[max_idx].is_hole = false;
190 contours[max_idx].points.reverse(); }
192}
193
194pub fn assign_holes_to_outers(contours: &[Contour]) -> Vec<(usize, Vec<usize>)> {
197 let outers: Vec<usize> = contours.iter().enumerate()
198 .filter(|(_, c)| !c.is_hole).map(|(i, _)| i).collect();
199 let holes: Vec<usize> = contours.iter().enumerate()
200 .filter(|(_, c)| c.is_hole).map(|(i, _)| i).collect();
201
202 let mut assignments: Vec<(usize, Vec<usize>)> = outers.iter()
203 .map(|&i| (i, Vec::new())).collect();
204
205 for &h in &holes {
206 if let Some(hp) = contours[h].points.first() {
207 let mut best = None;
209 let mut best_area = f32::MAX;
210 for &o in &outers {
211 if point_in_polygon(*hp, &contours[o].points) {
212 let area = signed_area(&contours[o].points).abs();
213 if area < best_area {
214 best_area = area;
215 best = Some(o);
216 }
217 }
218 }
219 if let Some(o) = best {
220 if let Some(entry) = assignments.iter_mut().find(|(idx, _)| *idx == o) {
221 entry.1.push(h);
222 }
223 }
224 }
225 }
226
227 assignments
228}
229
230pub fn simplify_contour(points: &[Vec2], tolerance: f32) -> Vec<Vec2> {
233 if points.len() <= 3 { return points.to_vec(); }
234 let mut keep = vec![false; points.len()];
235 keep[0] = true;
236 keep[points.len() - 1] = true;
237 rdp_recursive(points, 0, points.len() - 1, tolerance * tolerance, &mut keep);
238 points.iter().enumerate().filter(|(i, _)| keep[*i]).map(|(_, p)| *p).collect()
239}
240
241fn rdp_recursive(points: &[Vec2], start: usize, end: usize, tol_sq: f32, keep: &mut [bool]) {
242 if end <= start + 1 { return; }
243 let line_dir = points[end] - points[start];
244 let line_len_sq = line_dir.length_squared();
245 let mut max_dist_sq = 0.0f32;
246 let mut max_idx = start;
247 for i in (start + 1)..end {
248 let d = if line_len_sq < 1e-10 {
249 (points[i] - points[start]).length_squared()
250 } else {
251 let t = ((points[i] - points[start]).dot(line_dir) / line_len_sq).clamp(0.0, 1.0);
252 let proj = points[start] + line_dir * t;
253 (points[i] - proj).length_squared()
254 };
255 if d > max_dist_sq {
256 max_dist_sq = d;
257 max_idx = i;
258 }
259 }
260 if max_dist_sq > tol_sq {
261 keep[max_idx] = true;
262 rdp_recursive(points, start, max_idx, tol_sq, keep);
263 rdp_recursive(points, max_idx, end, tol_sq, keep);
264 }
265}
266
267pub fn extract_outline(font: &FontVec, ch: char, scale: f32) -> Option<GlyphOutline> {
271 let glyph_id = font.glyph_id(ch);
272 if glyph_id.0 == 0 && ch != ' ' { return None; }
273
274 let px_scale = PxScale::from(scale);
275 let scaled = font.as_scaled(px_scale);
276 let advance = scaled.h_advance(glyph_id);
277 let ascent = scaled.ascent();
278
279 let glyph = glyph_id.with_scale_and_position(px_scale, ab_glyph::point(0.0, ascent));
280
281 let outlined = font.outline_glyph(glyph)?;
282 let bounds_ab = outlined.px_bounds();
283
284 let mut contours = Vec::new();
285 let mut current_contour: Vec<Vec2> = Vec::new();
286 let mut last_pos = Vec2::ZERO;
287
288 struct OutlineBuilder {
295 contours: Vec<Vec<Vec2>>,
296 current: Vec<Vec2>,
297 tolerance: f32,
298 }
299
300 impl OutlineBuilder {
301 fn finish_contour(&mut self) {
302 if self.current.len() >= 3 {
303 self.contours.push(std::mem::take(&mut self.current));
304 } else {
305 self.current.clear();
306 }
307 }
308 }
309
310 let b = bounds_ab;
317 let w = (b.max.x - b.min.x).ceil() as u32 + 2;
318 let h = (b.max.y - b.min.y).ceil() as u32 + 2;
319
320 if w < 2 || h < 2 { return None; }
321
322 let mut coverage = vec![0.0f32; (w * h) as usize];
324 let ox = b.min.x.floor();
325 let oy = b.min.y.floor();
326
327 outlined.draw(|x, y, v| {
328 let px = x as i32 - ox as i32;
329 let py = y as i32 - oy as i32;
330 if px >= 0 && py >= 0 && (px as u32) < w && (py as u32) < h {
331 coverage[(py as u32 * w + px as u32) as usize] = v;
332 }
333 });
334
335 let threshold = 0.5;
337 let contour_points = marching_squares_contour(&coverage, w as usize, h as usize, threshold);
338
339 for mut pts in contour_points {
340 if pts.len() < 3 { continue; }
341 for p in &mut pts {
343 p.x = p.x + ox;
344 p.y = p.y + oy;
345 }
346 contours.push(Contour { points: pts, is_hole: false });
347 }
348
349 if contours.is_empty() {
350 contours.push(Contour {
352 points: vec![
353 Vec2::new(b.min.x, b.min.y),
354 Vec2::new(b.max.x, b.min.y),
355 Vec2::new(b.max.x, b.max.y),
356 Vec2::new(b.min.x, b.max.y),
357 ],
358 is_hole: false,
359 });
360 }
361
362 classify_contours(&mut contours);
363
364 let all_points: Vec<Vec2> = contours.iter().flat_map(|c| c.points.iter().copied()).collect();
365 let gbounds = if all_points.is_empty() {
366 GlyphBounds { min: Vec2::new(b.min.x, b.min.y), max: Vec2::new(b.max.x, b.max.y) }
367 } else {
368 GlyphBounds::from_points(&all_points)
369 };
370
371 Some(GlyphOutline { contours, advance_width: advance, bounds: gbounds })
372}
373
374fn marching_squares_contour(coverage: &[f32], w: usize, h: usize, threshold: f32) -> Vec<Vec<Vec2>> {
376 if w < 2 || h < 2 { return Vec::new(); }
377
378 let mut visited = vec![false; w * h];
379 let mut contours = Vec::new();
380
381 for y in 1..h - 1 {
383 for x in 1..w - 1 {
384 let idx = y * w + x;
385 if visited[idx] { continue; }
386 if coverage[idx] < threshold { continue; }
387
388 let is_boundary = (x > 0 && coverage[idx - 1] < threshold)
390 || (x + 1 < w && coverage[idx + 1] < threshold)
391 || (y > 0 && coverage[idx - w] < threshold)
392 || (y + 1 < h && coverage[idx + w] < threshold);
393
394 if !is_boundary { continue; }
395
396 let contour = trace_boundary(coverage, w, h, x, y, threshold, &mut visited);
398 if contour.len() >= 3 {
399 contours.push(contour);
400 }
401 }
402 }
403
404 contours
405}
406
407fn trace_boundary(
409 coverage: &[f32], w: usize, h: usize,
410 sx: usize, sy: usize, threshold: f32,
411 visited: &mut [bool],
412) -> Vec<Vec2> {
413 let mut contour = Vec::new();
414 let dirs: [(i32, i32); 8] = [
415 (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1),
416 ];
417
418 let mut x = sx as i32;
419 let mut y = sy as i32;
420 let mut dir = 0usize;
421 let max_steps = w * h;
422
423 for step in 0..max_steps {
424 if step > 0 && x == sx as i32 && y == sy as i32 { break; }
425
426 contour.push(Vec2::new(x as f32 + 0.5, y as f32 + 0.5));
427 let idx = y as usize * w + x as usize;
428 visited[idx] = true;
429
430 let mut found = false;
432 for i in 0..8 {
433 let d = (dir + 6 + i) % 8; let nx = x + dirs[d].0;
435 let ny = y + dirs[d].1;
436 if nx >= 0 && ny >= 0 && (nx as usize) < w && (ny as usize) < h {
437 let nidx = ny as usize * w + nx as usize;
438 if coverage[nidx] >= threshold {
439 let is_bd = (nx > 0 && coverage[nidx - 1] < threshold)
440 || ((nx as usize + 1) < w && coverage[nidx + 1] < threshold)
441 || (ny > 0 && coverage[nidx - w as i32 as usize] < threshold)
442 || ((ny as usize + 1) < h && coverage[nidx + w] < threshold);
443 if is_bd || !found {
444 x = nx;
445 y = ny;
446 dir = d;
447 found = true;
448 break;
449 }
450 }
451 }
452 }
453 if !found { break; }
454 }
455
456 if contour.len() > 20 {
458 simplify_contour(&contour, 0.5)
459 } else {
460 contour
461 }
462}
463
464pub struct OutlineCache {
468 pub outlines: HashMap<char, GlyphOutline>,
469}
470
471impl OutlineCache {
472 pub fn build(font: &FontVec, chars: &[char], scale: f32) -> Self {
473 let mut outlines = HashMap::new();
474 for &ch in chars {
475 if let Some(outline) = extract_outline(font, ch, scale) {
476 outlines.insert(ch, outline);
477 }
478 }
479 Self { outlines }
480 }
481
482 pub fn get(&self, ch: char) -> Option<&GlyphOutline> {
483 self.outlines.get(&ch)
484 }
485
486 pub fn len(&self) -> usize { self.outlines.len() }
487 pub fn is_empty(&self) -> bool { self.outlines.is_empty() }
488}
489
490#[cfg(test)]
493mod tests {
494 use super::*;
495
496 fn square() -> Vec<Vec2> {
497 vec![
498 Vec2::new(0.0, 0.0),
499 Vec2::new(1.0, 0.0),
500 Vec2::new(1.0, 1.0),
501 Vec2::new(0.0, 1.0),
502 ]
503 }
504
505 #[test]
506 fn signed_area_ccw() {
507 let area = signed_area(&square());
508 assert!(area > 0.0, "CCW square should have positive area: {}", area);
509 }
510
511 #[test]
512 fn signed_area_cw() {
513 let mut sq = square();
514 sq.reverse();
515 let area = signed_area(&sq);
516 assert!(area < 0.0, "CW square should have negative area: {}", area);
517 }
518
519 #[test]
520 fn point_in_polygon_inside() {
521 assert!(point_in_polygon(Vec2::new(0.5, 0.5), &square()));
522 }
523
524 #[test]
525 fn point_in_polygon_outside() {
526 assert!(!point_in_polygon(Vec2::new(2.0, 2.0), &square()));
527 }
528
529 #[test]
530 fn subdivide_quadratic_produces_points() {
531 let pts = subdivide_quadratic(
532 Vec2::new(0.0, 0.0), Vec2::new(0.5, 1.0), Vec2::new(1.0, 0.0), 0.1,
533 );
534 assert!(pts.len() >= 3, "Should produce at least 3 points");
535 }
536
537 #[test]
538 fn subdivide_cubic_produces_points() {
539 let pts = subdivide_cubic(
540 Vec2::new(0.0, 0.0), Vec2::new(0.3, 1.0),
541 Vec2::new(0.7, 1.0), Vec2::new(1.0, 0.0), 0.1,
542 );
543 assert!(pts.len() >= 4, "Should produce at least 4 points");
544 }
545
546 #[test]
547 fn classify_contours_identifies_holes() {
548 let outer = vec![
549 Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0),
550 Vec2::new(10.0, 10.0), Vec2::new(0.0, 10.0),
551 ];
552 let hole = vec![
553 Vec2::new(2.0, 2.0), Vec2::new(8.0, 2.0),
554 Vec2::new(8.0, 8.0), Vec2::new(2.0, 8.0),
555 ];
556 let mut contours = vec![
558 Contour { points: outer.into_iter().rev().collect(), is_hole: false },
559 Contour { points: hole, is_hole: false },
560 ];
561 classify_contours(&mut contours);
562 let outers: Vec<_> = contours.iter().filter(|c| !c.is_hole).collect();
563 let holes: Vec<_> = contours.iter().filter(|c| c.is_hole).collect();
564 assert_eq!(outers.len(), 1);
565 assert_eq!(holes.len(), 1);
566 }
567
568 #[test]
569 fn assign_holes_finds_containment() {
570 let outer = Contour {
571 points: vec![
572 Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0),
573 Vec2::new(10.0, 10.0), Vec2::new(0.0, 10.0),
574 ],
575 is_hole: false,
576 };
577 let hole = Contour {
578 points: vec![
579 Vec2::new(3.0, 3.0), Vec2::new(7.0, 3.0),
580 Vec2::new(7.0, 7.0), Vec2::new(3.0, 7.0),
581 ],
582 is_hole: true,
583 };
584 let assignments = assign_holes_to_outers(&[outer, hole]);
585 assert_eq!(assignments.len(), 1);
586 assert_eq!(assignments[0].1.len(), 1);
587 }
588
589 #[test]
590 fn simplify_reduces_points() {
591 let pts: Vec<Vec2> = (0..100).map(|i| {
592 let t = i as f32 / 99.0;
593 Vec2::new(t * 10.0, (t * 6.28).sin() * 0.01)
594 }).collect();
595 let simplified = simplify_contour(&pts, 0.1);
596 assert!(simplified.len() < pts.len());
597 }
598
599 #[test]
600 fn glyph_bounds_from_points() {
601 let pts = vec![Vec2::new(-1.0, -2.0), Vec2::new(3.0, 4.0)];
602 let b = GlyphBounds::from_points(&pts);
603 assert_eq!(b.min, Vec2::new(-1.0, -2.0));
604 assert_eq!(b.max, Vec2::new(3.0, 4.0));
605 }
606}