1use std::collections::BTreeMap;
7
8use kurbo::{Affine, Line, ParamCurve, ParamCurveArclen, ParamCurveNearest, PathSeg, Point};
9use smol_str::SmolStr;
10use thiserror::Error;
11use write_fonts::OtRound;
12
13use crate::font::{Alignment, Glyph, Hint, HintType, Layer, Node, NodeType, Path, Shape};
14
15impl OtRound<Node> for Node {
16 fn ot_round(self) -> Node {
17 let (x, y) = self.pt.ot_round();
18 Node {
19 pt: Point::new(x as _, y as _),
20 node_type: self.node_type,
21 }
22 }
23}
24
25#[derive(Debug, Error, Clone)]
26#[error("component '{component}' failed: {reason}")]
27pub struct BadCornerComponent {
28 component: SmolStr,
29 reason: BadCornerComponentReason,
30}
31
32#[derive(Debug, Error, Clone)]
33pub enum BadCornerComponentReason {
34 #[error("corner glyph contains no layer '{0}'")]
35 MissingLayer(SmolStr),
36 #[error("glyph contains no paths")]
37 NoPaths,
38 #[error("no path at shape index '{0}'")]
39 BadShapeIndex(usize),
40 #[error("path contains too few points")]
41 PathTooShort,
42}
43
44impl BadCornerComponentReason {
45 fn add_name(self, component: SmolStr) -> BadCornerComponent {
47 BadCornerComponent {
48 component,
49 reason: self,
50 }
51 }
52}
53
54fn point_on_seg_at_distance(seg: PathSeg, distance: f64) -> f64 {
58 seg.inv_arclen(distance, 1e-6)
59}
60
61pub(crate) fn insert_corner_components_for_layer(
63 layer: &mut Layer,
64 glyphs: &BTreeMap<SmolStr, Glyph>,
65) -> Result<(), BadCornerComponent> {
66 let mut corner_hints: Vec<Hint> = layer
67 .hints
68 .iter()
69 .filter(|h| h.type_ == HintType::Corner)
70 .cloned()
71 .collect();
72
73 corner_hints.sort_by_key(|hint| (hint.shape_index, hint.node_index));
75
76 if corner_hints.is_empty() {
77 return Ok(());
78 }
79
80 let mut inserted_pts = 0;
83 let mut current_shape = 0;
84
85 for hint in corner_hints {
86 if hint.shape_index != current_shape {
87 current_shape = hint.shape_index;
88 inserted_pts = 0;
89 }
90
91 let Some(corner_glyph) = glyphs.get(&hint.name) else {
92 log::warn!("corner component '{}' not found", hint.name);
93 continue;
94 };
95
96 let component = corner_glyph
97 .layers
98 .iter()
99 .find(|l| l.layer_id == layer.master_id())
100 .ok_or_else(|| BadCornerComponentReason::MissingLayer(layer.master_id().into()))
101 .and_then(CornerComponent::new)
102 .map_err(|e| e.add_name(hint.name.clone()))?;
103
104 let n_points = component.corner_path.nodes.len() - 1;
105 layer
106 .insert_corner_component(component, &hint, inserted_pts)
107 .map_err(|e| e.add_name(hint.name.clone()))?;
108 inserted_pts += n_points;
109 }
110
111 layer.hints.retain(|h| h.type_ != HintType::Corner);
113
114 Ok(())
115}
116
117impl Layer {
118 fn insert_corner_component(
120 &mut self,
121 mut component: CornerComponent,
122 hint: &Hint,
123 delta_pt_index: usize,
124 ) -> Result<(), BadCornerComponentReason> {
125 let path = match self.shapes.get_mut(hint.shape_index) {
126 Some(Shape::Path(p)) => p,
127 _ => return Err(BadCornerComponentReason::BadShapeIndex(hint.shape_index)),
128 };
129
130 let point_idx = (hint.node_index + delta_pt_index) % path.nodes.len();
131 let scale = Affine::scale_non_uniform(hint.scale.x.0, hint.scale.y.0);
132 component.apply_transform(scale);
134
135 let AlignmentState {
136 mut instroke_pt,
137 outstroke_pt: _,
138 correction,
139 } = component.align_to_main_path(path, hint, point_idx);
141
142 let original_outstroke = path.get_next_segment(point_idx).unwrap();
143 if hint.alignment != Alignment::InStroke && correction {
144 instroke_pt = component
145 .recompute_instroke_intersection_point(path, point_idx)
146 .unwrap_or(instroke_pt);
147
148 if !matches!(
149 component.corner_path.get_next_segment(0).unwrap(),
150 PathSeg::Line(_)
151 ) {
152 component.stretch_first_seg_to_fit(instroke_pt);
153 }
154 }
155 path.split_instroke(point_idx, instroke_pt);
158 let insert_pt = path.next_idx(point_idx);
160 path.nodes.splice(
161 insert_pt..insert_pt,
162 component
163 .corner_path
164 .nodes
165 .iter()
166 .cloned()
167 .skip(1)
168 .map(|node| node.ot_round()),
169 );
170
171 let added_points = component.corner_path.nodes.len() - 1;
172 let new_outstroke_idx = path.prev_idx(insert_pt + added_points);
174
175 if let Some(outstroke_intersection_point) =
177 component.recompute_outstroke_intersection_point(original_outstroke, hint)
178 {
179 path.fixup_outstroke(
180 original_outstroke,
181 outstroke_intersection_point,
182 new_outstroke_idx,
183 );
184 }
185
186 for mut path in component.other_paths.into_iter() {
188 path.nodes
189 .iter_mut()
190 .for_each(|node| *node = node.ot_round());
191 self.shapes.push(Shape::Path(path));
192 }
193
194 Ok(())
195 }
196}
197
198impl Hint {
199 fn is_flipped(&self) -> bool {
200 self.scale.x.0 * self.scale.y.0 < 0.0
201 }
202}
203
204impl Path {
205 fn set_point(&mut self, idx: usize, point: Point) {
206 self.nodes[idx].pt = point;
207 self.nodes[idx] = self.nodes[idx].ot_round();
208 }
209
210 fn split_instroke(&mut self, point_idx: usize, intersection: Point) {
213 let instroke = self.get_previous_segment(point_idx).unwrap();
214 let nearest_t = instroke.nearest(intersection, 1e-6).t;
215 let split = instroke.subsegment(0.0..nearest_t);
216 match split {
217 PathSeg::Line(line) => self.set_point(point_idx, line.p1),
218 PathSeg::Quad(quad) => {
219 self.set_point(point_idx, quad.p2);
220 let idx = self.prev_idx(point_idx);
221 self.set_point(idx, quad.p1);
222 }
223 PathSeg::Cubic(cubic) => {
224 self.set_point(point_idx, cubic.p3);
225 let idx = self.prev_idx(point_idx);
226 self.set_point(idx, cubic.p2);
227 let idx = self.prev_idx(idx);
228 self.set_point(idx, cubic.p1);
229 }
230 };
231 }
232
233 fn fixup_outstroke(&mut self, original: PathSeg, intersection: Point, point_idx: usize) {
234 let nearest_t = original.nearest(intersection, 1e-6).t;
235 let split = original.subsegment(nearest_t..1.0);
236 match split {
237 PathSeg::Line(line) => self.set_point(point_idx, line.p0),
238 PathSeg::Quad(quad) => {
239 self.set_point(point_idx, quad.p0);
240 let idx = self.next_idx(point_idx);
241 self.set_point(idx, quad.p1);
242 }
243 PathSeg::Cubic(cubic) => {
244 self.set_point(point_idx, cubic.p0);
245 let idx = self.next_idx(point_idx);
246 self.set_point(idx, cubic.p1);
247 let idx = self.next_idx(idx);
248 self.set_point(idx, cubic.p2);
249 }
250 }
251 }
252}
253
254struct CornerComponent {
255 corner_path: Path,
256 other_paths: Vec<Path>,
257 #[expect(dead_code)] left: Point,
260 #[expect(dead_code)] right: Point,
263}
264
265impl CornerComponent {
266 fn new(corner_layer: &Layer) -> Result<Self, BadCornerComponentReason> {
267 let origin = corner_layer.get_anchor_pt("origin").unwrap_or_default();
268 let left = corner_layer.get_anchor_pt("left").unwrap_or_default();
269 let right = corner_layer.get_anchor_pt("right").unwrap_or_default();
270
271 let mut path_iter = corner_layer
273 .shapes
274 .iter()
275 .filter_map(Shape::as_path)
276 .cloned()
277 .map(|mut path| {
279 path.nodes
280 .iter_mut()
281 .for_each(|node| node.pt -= origin.to_vec2());
282 path
283 });
284
285 let corner_path = path_iter.next().ok_or(BadCornerComponentReason::NoPaths)?;
286 if corner_path.nodes.len() < 2 {
287 return Err(BadCornerComponentReason::PathTooShort);
288 }
289 let other_paths = path_iter.collect::<Vec<_>>();
290
291 Ok(Self {
292 corner_path,
293 other_paths,
294 left,
295 right,
296 })
297 }
298
299 fn apply_transform(&mut self, transform: Affine) {
300 for node in self.corner_path.nodes.iter_mut().chain(
301 self.other_paths
302 .iter_mut()
303 .flat_map(|path| path.nodes.iter_mut()),
304 ) {
305 node.pt = transform * node.pt;
306 }
307 }
308
309 fn last_point(&self) -> Point {
310 self.corner_path.nodes.last().unwrap().pt
312 }
313
314 fn reverse_corner_path(&mut self) {
315 self.corner_path.reverse();
316 let [.., p0, pn] = self.corner_path.nodes.as_mut_slice() else {
320 return;
321 };
322 if p0.node_type == NodeType::OffCurve {
323 pn.node_type = match pn.node_type {
324 NodeType::Line => NodeType::Curve,
325 NodeType::LineSmooth => NodeType::CurveSmooth,
326 other => other,
327 };
328 }
329 }
330
331 fn align_to_main_path(&mut self, path: &Path, hint: &Hint, point_idx: usize) -> AlignmentState {
333 let mut angle = (-self.last_point().y).atan2(self.last_point().x);
334 if hint.is_flipped() {
335 angle += std::f64::consts::FRAC_PI_2;
336 self.reverse_corner_path();
337 }
338
339 let instroke = path.get_previous_segment(point_idx).unwrap();
340 let outstroke = path.get_next_segment(point_idx).unwrap();
341 let target_pt = path.nodes.get(point_idx).unwrap().pt;
342
343 let distance = if hint.is_flipped() {
345 self.last_point().y
346 } else {
347 self.last_point().x
348 };
349
350 let outstroke_t = point_on_seg_at_distance(outstroke, distance.abs());
351 let outstroke_pt = outstroke.eval(outstroke_t);
352 let outstroke_angle = (outstroke_pt - target_pt).angle();
353
354 let distance = if hint.is_flipped() {
356 -self.corner_path.nodes.first().unwrap().pt.x
357 } else {
358 self.corner_path.nodes.first().unwrap().pt.y
359 };
360
361 let instroke_t = point_on_seg_at_distance(instroke, distance.abs());
362 let instroke_pt = instroke.reverse().eval(instroke_t);
363 let instroke_angle = (target_pt - instroke_pt).angle() + std::f64::consts::FRAC_PI_2;
364
365 let correction = !(py_is_close(instroke_t, 0.0) || py_is_close(instroke_t, 1.0));
366
367 let angle = angle
368 + match hint.alignment {
369 Alignment::OutStroke => outstroke_angle,
370 Alignment::InStroke => instroke_angle,
371 Alignment::Middle => (instroke_angle + outstroke_angle) / 2.0,
372 _ => 0.0,
373 };
374
375 let xform = Affine::translate(target_pt.to_vec2()).pre_rotate(angle);
379 self.apply_transform(xform);
380
381 AlignmentState {
382 instroke_pt,
383 outstroke_pt,
384 correction,
385 }
386 }
387
388 fn recompute_instroke_intersection_point(
390 &self,
391 path: &Path,
392 target_node_ix: usize,
393 ) -> Option<Point> {
394 let first_seg_as_line = &self.corner_path.nodes.as_slice()[..2];
396 let first_seg_as_line = Line::new(first_seg_as_line[0].pt, first_seg_as_line[1].pt);
397 let instroke = path.get_previous_segment(target_node_ix).unwrap();
398 unbounded_seg_seg_intersection(first_seg_as_line.into(), instroke)
399 }
400
401 fn recompute_outstroke_intersection_point(
403 &self,
404 original_outstroke: PathSeg,
405 hint: &Hint,
406 ) -> Option<Point> {
407 if hint.is_flipped() {
408 unbounded_seg_seg_intersection(
409 self.corner_path
410 .get_previous_segment(self.corner_path.nodes.len() - 1)
411 .unwrap(),
412 original_outstroke,
413 )
414 } else {
415 let nearest = original_outstroke.nearest(self.last_point(), 1e-6);
417 Some(original_outstroke.eval(nearest.t))
418 }
419 }
420
421 fn stretch_first_seg_to_fit(&mut self, intersection_pt: Point) {
422 let delta = intersection_pt - self.corner_path.nodes[0].pt;
423 self.corner_path.nodes[1].pt += delta;
424 }
425}
426
427fn unbounded_seg_seg_intersection(seg1: PathSeg, seg2: PathSeg) -> Option<Point> {
431 match (seg1, seg2) {
433 (PathSeg::Line(one), PathSeg::Line(two)) => one.crossing_point(two),
434 (seg, PathSeg::Line(line)) | (PathSeg::Line(line), seg) => {
435 const LITERALLY_UNBOUNDED: f64 = 1e9;
438
439 let direction = (line.p1 - line.p0).normalize();
441 let extended_line = Line::new(
442 line.p0 - direction * LITERALLY_UNBOUNDED,
443 line.p1 + direction * LITERALLY_UNBOUNDED,
444 );
445 seg.intersect_line(extended_line)
446 .first()
447 .map(|hit| seg.eval(hit.segment_t))
448 }
449 _ => None,
450 }
451}
452
453fn py_is_close(a: f64, b: f64) -> bool {
455 const REL_TOL: f64 = 1e-09;
457 (a - b).abs() <= REL_TOL * a.abs().max(b.abs())
458}
459
460struct AlignmentState {
461 instroke_pt: Point,
462 #[expect(dead_code, reason = "python does it")]
463 outstroke_pt: Point,
464 correction: bool,
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use crate::font::Font;
471 use rstest::rstest;
472 use std::path::{Path as FilePath, PathBuf};
473
474 fn testdata_dir() -> PathBuf {
475 let mut dir = FilePath::new("../resources/testdata");
476 if !dir.is_dir() {
477 dir = FilePath::new("./resources/testdata");
478 }
479 dir.to_path_buf()
480 }
481
482 fn glyphs3_dir() -> PathBuf {
483 testdata_dir().join("glyphs3")
484 }
485
486 fn compare_paths(test_layer: &Layer, expectation_layer: &Layer, glyph_name: &str) {
488 let test_paths: Vec<_> = test_layer
490 .shapes
491 .iter()
492 .filter_map(Shape::as_path)
493 .collect();
494
495 let expectation_paths: Vec<_> = expectation_layer
496 .shapes
497 .iter()
498 .filter_map(Shape::as_path)
499 .collect();
500
501 assert_eq!(
502 test_paths.len(),
503 expectation_paths.len(),
504 "Number of paths differs for glyph '{glyph_name}': expected {}, got {}",
505 expectation_paths.len(),
506 test_paths.len()
507 );
508
509 for (i, (test_path, expectation_path)) in
510 test_paths.iter().zip(expectation_paths.iter()).enumerate()
511 {
512 assert_eq!(
513 test_path.to_points(),
514 expectation_path.to_points(),
515 "Path {i} differs for glyph '{glyph_name}'",
516 );
517 }
518 }
519
520 fn test_corner_component_glyph(glyph_name: &str) {
521 let font_path = glyphs3_dir().join("CornerComponents.glyphs");
522 let font = Font::load_raw(&font_path).expect("Failed to load CornerComponents.glyphs");
523
524 let mut test_glyph = font
525 .glyphs
526 .get(glyph_name)
527 .cloned()
528 .unwrap_or_else(|| panic!("Test glyph '{}' not found", glyph_name));
529
530 let expectation_glyph_name = format!("{}.expectation", glyph_name);
531
532 for layer in &mut test_glyph.layers {
534 insert_corner_components_for_layer(layer, &font.glyphs)
535 .expect("Failed to insert corner components");
536 }
537
538 let expectation_glyph = font
539 .glyphs
540 .get(expectation_glyph_name.as_str())
541 .unwrap_or_else(|| panic!("Expectation glyph '{}' not found", expectation_glyph_name));
542
543 assert!(
545 !test_glyph.layers.is_empty(),
546 "Test glyph '{glyph_name}' has no layers",
547 );
548 assert!(
549 !expectation_glyph.layers.is_empty(),
550 "Expectation glyph '{}' has no layers",
551 expectation_glyph_name
552 );
553
554 let test_layer = &test_glyph.layers[0];
555 let expectation_layer = &expectation_glyph.layers[0];
556
557 compare_paths(test_layer, expectation_layer, glyph_name);
559 }
560
561 #[rstest]
562 #[case::aa_simple_angleinstroke("aa_simple_angleinstroke")]
563 #[case::ab_simple_angled("ab_simple_angled")]
564 #[case::ac_scale("ac_scale")]
565 #[case::ad_curved_instroke("ad_curved_instroke")]
566 #[case::ae_curved_corner_firstseg("ae_curved_corner_firstseg")]
567 #[case::af_curved_corner_firstseg_slanted("af_curved_corner_firstseg_slanted")]
568 #[case::ag_curved_corner_bothsegs("ag_curved_corner_bothsegs")]
569 #[case::ag_curved_corner_bothsegs_rotated("ag_curved_corner_bothsegs_rotated")]
570 #[case::ah_origin("ah_origin")]
571 #[case::ai_curved_outstroke("ai_curved_outstroke")]
572 #[case::aj_right_alignment("aj_right_alignment")]
573 #[case::ak_right_slanted("ak_right_slanted")]
574 #[case::al_unaligned("al_unaligned")]
575 #[case::am_middle("am_middle")]
576 #[case::an_flippy("an_flippy")]
577 #[case::ao_firstnode("ao_firstnode")]
578 #[case::ap_twoofthem("ap_twoofthem")]
579 #[case::aq_rightleg("aq_rightleg")]
580 #[case::ar_leftleg("ar_leftleg")]
581 #[case::as_closedpaths("as_closedpaths")]
582 #[case::at_unaligned_lastseg("at_unaligned_lastseg")]
583 #[case::au_left_anchoronpath("au_left_anchoronpath")]
584 #[case::av_left_anchoroffpath("av_left_anchoroffpath")]
585 #[case::aw_direction("aw_direction")]
586 #[case::ax_curved_instroke2("ax_curved_instroke2")]
587 fn test_corner_components(#[case] glyph_name: &str) {
589 let _ = env_logger::builder().is_test(true).try_init();
590 if glyph_name.contains("left_anchor") {
592 log::info!(
594 "Skipping '{}': left anchors not quite working yet",
595 glyph_name
596 );
597 return;
598 }
599
600 test_corner_component_glyph(glyph_name);
601 }
602}