1use geop_core_geometry::nurb_curve::NurbCurve2D;
22use geop_core_math::{
23 geop_error::{GeopError, GeopResult},
24 scalars::Scalar,
25 vector::Vector2,
26};
27use ttf_parser::{Face, GlyphId, OutlineBuilder};
28
29use crate::profile::spline_knots;
30
31static FONT: &[u8] = include_bytes!("../fonts/NotoSans-Regular-subset.ttf");
33
34pub type FontPoint = [f64; 2];
36
37#[derive(Clone, Debug, PartialEq)]
41pub struct OutlinePiece {
42 pub degree: usize,
43 pub points: Vec<FontPoint>,
44}
45
46#[derive(Clone, Debug, PartialEq)]
50pub struct Contour {
51 pub pieces: Vec<OutlinePiece>,
52}
53
54fn font() -> GeopResult<Face<'static>> {
55 Face::parse(FONT, 0).map_err(|e| GeopError::new(format!("the sketch font: {e}")))
56}
57
58pub fn outlines(text: &str) -> GeopResult<Vec<Contour>> {
62 let face = font()?;
63 let line =
64 f64::from(face.ascender()) - f64::from(face.descender()) + f64::from(face.line_gap());
65 let mut contours = Vec::new();
66 let mut pen = [0.0, 0.0];
67 for c in text.chars() {
68 if c == '\n' {
69 pen = [0.0, pen[1] - line];
70 continue;
71 }
72 let glyph = face.glyph_index(c).unwrap_or(GlyphId(0));
73 let mut outline = Outline {
74 at: pen,
75 contours: Vec::new(),
76 current: Vec::new(),
77 start: [0.0, 0.0],
78 last: [0.0, 0.0],
79 };
80 face.outline_glyph(glyph, &mut outline);
81 outline.close();
82 contours.extend(outline.contours);
83 pen[0] += f64::from(face.glyph_hor_advance(glyph).unwrap_or(0));
84 }
85 Ok(contours)
86}
87
88fn capital_height() -> GeopResult<f64> {
91 let face = font()?;
92 let height = face
93 .capital_height()
94 .ok_or_else(|| GeopError::new("the sketch font gives no capital height"))?;
95 Ok(f64::from(height))
96}
97
98pub fn place(
102 contours: &[Contour],
103 anchor: Vector2,
104 height: Scalar,
105 angle: Scalar,
106) -> GeopResult<Vec<Vec<NurbCurve2D>>> {
107 let scale = height.div(Scalar::from_f64(capital_height()?))?;
108 let (cos, sin) = (angle.cos(), angle.sin());
109 let at = |p: &FontPoint| {
110 let local = Vector2::from_array([
111 Scalar::from_f64(p[0]).mul(scale),
112 Scalar::from_f64(p[1]).mul(scale),
113 ]);
114 anchor.add(&local.rotate(cos, sin))
115 };
116 (contours.iter())
117 .map(|contour| {
118 (contour.pieces.iter())
119 .map(|piece| {
120 let n = piece.points.len();
121 NurbCurve2D::try_new(
122 piece.degree,
123 piece
124 .points
125 .iter()
126 .map(|p| at(p).homogeneous(Scalar::ONE))
127 .collect(),
128 spline_knots(n, piece.degree)?,
129 )
130 })
131 .collect()
132 })
133 .collect()
134}
135
136struct Outline {
139 at: FontPoint,
140 contours: Vec<Contour>,
141 current: Vec<OutlinePiece>,
143 start: FontPoint,
145 last: FontPoint,
147}
148
149impl Outline {
150 fn point(&self, x: f32, y: f32) -> FontPoint {
151 [self.at[0] + f64::from(x), self.at[1] + f64::from(y)]
152 }
153
154 fn push(&mut self, degree: usize, points: &[FontPoint]) {
160 let from = self.last;
161 let to = *points.last().expect("a segment ends somewhere");
162 if points.iter().all(|&p| p == from) {
163 return;
164 }
165 if degree == 2 && (points[0] == from || points[0] == to) {
166 return self.push(1, &[to]);
167 }
168 self.last = to;
169 if degree == 2
170 && let Some(previous) = self.current.last_mut()
171 && previous.degree == 2
172 {
173 let control = previous.points[previous.points.len() - 2];
174 let halfway = [
175 (control[0] + points[0][0]) / 2.0,
176 (control[1] + points[0][1]) / 2.0,
177 ];
178 if halfway == from {
179 previous.points.pop();
180 previous.points.extend_from_slice(points);
181 return;
182 }
183 }
184 let mut all = vec![from];
185 all.extend_from_slice(points);
186 self.current.push(OutlinePiece {
187 degree,
188 points: all,
189 });
190 }
191
192 fn close(&mut self) {
196 if self.last != self.start {
197 self.push(1, &[self.start]);
198 }
199 let mut pieces = std::mem::take(&mut self.current);
200 if let [only] = &pieces[..] {
201 pieces = halves(only);
202 }
203 if pieces.len() >= 2 {
204 self.contours.push(Contour { pieces });
205 }
206 }
207}
208
209fn halves(piece: &OutlinePiece) -> Vec<OutlinePiece> {
213 let p = &piece.points;
214 let mid = |a: FontPoint, b: FontPoint| [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0];
215 let segments = p.len() - piece.degree;
216 let (first, second) = if piece.degree == 2 && segments >= 2 {
217 let m = segments / 2;
218 let joint = mid(p[m], p[m + 1]);
219 let mut first = p[..=m].to_vec();
220 first.push(joint);
221 let mut second = vec![joint];
222 second.extend_from_slice(&p[m + 1..]);
223 (first, second)
224 } else {
225 let mut rows = vec![p.clone()];
227 while rows.last().expect("a first row").len() > 1 {
228 let row = rows.last().expect("a row");
229 let next = row.windows(2).map(|w| mid(w[0], w[1])).collect();
230 rows.push(next);
231 }
232 let first = rows.iter().map(|row| row[0]).collect();
233 let second = rows
234 .iter()
235 .rev()
236 .map(|row| *row.last().expect("a point"))
237 .collect();
238 (first, second)
239 };
240 [first, second]
241 .into_iter()
242 .map(|points| OutlinePiece {
243 degree: piece.degree,
244 points,
245 })
246 .collect()
247}
248
249impl OutlineBuilder for Outline {
250 fn move_to(&mut self, x: f32, y: f32) {
251 self.close();
252 self.start = self.point(x, y);
253 self.last = self.start;
254 }
255
256 fn line_to(&mut self, x: f32, y: f32) {
257 let p = self.point(x, y);
258 self.push(1, &[p]);
259 }
260
261 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
262 let points = [self.point(x1, y1), self.point(x, y)];
263 self.push(2, &points);
264 }
265
266 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
267 let points = [self.point(x1, y1), self.point(x2, y2), self.point(x, y)];
268 self.push(3, &points);
269 }
270
271 fn close(&mut self) {
272 Outline::close(self);
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use geop_core_geometry::nurb_curve::loop_signed_area;
280
281 fn closes(contour: &Contour) -> bool {
282 let n = contour.pieces.len();
283 let p = &contour.pieces;
284 (0..n).all(|i| p[i].points.last() == p[(i + 1) % n].points.first())
285 }
286
287 #[test]
290 fn letters_have_their_contours() {
291 for (text, count) in [("o", 2), ("B", 3), ("i", 2), (" ", 0), ("8", 3), ("A", 2)] {
292 let contours = outlines(text).unwrap();
293 assert_eq!(contours.len(), count, "{text:?}");
294 assert!(contours.iter().all(closes), "{text:?}");
295 }
296 }
297
298 #[test]
301 fn smooth_runs_are_one_piece() {
302 let contours = outlines("o").unwrap();
303 let pieces: usize = contours.iter().map(|c| c.pieces.len()).sum();
304 assert!(pieces <= 8, "{contours:?}");
305 }
306
307 #[test]
310 fn a_hole_winds_against_its_outline() {
311 let contours = outlines("o").unwrap();
312 let origin = Vector2::from_array([Scalar::from_f64(10.0), Scalar::from_f64(-3.0)]);
313 let placed = place(
314 &contours,
315 origin,
316 Scalar::from_f64(5.0),
317 Scalar::from_f64(0.3),
318 )
319 .unwrap();
320 let areas: Vec<f64> = placed
321 .iter()
322 .map(|c| loop_signed_area(c).unwrap().to_f64())
323 .collect();
324 assert!(areas[0] * areas[1] < 0.0, "{areas:?}");
325 }
326
327 #[test]
329 fn a_new_line_goes_down() {
330 let one = outlines("I").unwrap();
331 let two = outlines("I\nI").unwrap();
332 assert_eq!(two.len(), 2 * one.len());
333 let lowest = |c: &[Contour]| {
334 c.iter()
335 .flat_map(|c| &c.pieces)
336 .flat_map(|p| &p.points)
337 .map(|p| p[1])
338 .fold(f64::INFINITY, f64::min)
339 };
340 assert!(lowest(&two) < lowest(&one) - 500.0);
341 }
342}