glyphs_reader/
propagate_anchors.rs

1//! Propagating anchors from components to their composites
2//!
3//! Glyphs.app has a nice feature where anchors defined in the components
4//! of composite glyphs are copied into the composites themselves. This feature
5//! is not very extensively documented, and the code here is based off the
6//! Objective-C implementation, which was shared with us privately.
7
8use std::collections::{BTreeMap, HashMap};
9
10use indexmap::IndexMap;
11use kurbo::{Affine, Vec2};
12use smol_str::{SmolStr, format_smolstr};
13
14use crate::{
15    Component, Font, Glyph, Layer, Shape,
16    font::Anchor,
17    glyphdata::{Category, Subcategory},
18};
19
20impl fontdrasil::util::CompositeLike for Glyph {
21    fn name(&self) -> SmolStr {
22        self.name.clone()
23    }
24
25    fn has_components(&self) -> bool {
26        Glyph::has_components(self)
27    }
28
29    fn component_names(&self) -> impl Iterator<Item = SmolStr> {
30        self.layers
31            .iter()
32            .chain(self.bracket_layers.iter())
33            .flat_map(|layer| layer.shapes.iter())
34            .filter_map(|shape| match shape {
35                Shape::Path(..) => None,
36                Shape::Component(c) => Some(c.name.clone()),
37            })
38    }
39}
40
41impl Font {
42    /// Copy anchors from component glyphs into their including composites
43    pub fn propagate_all_anchors(&mut self) {
44        propagate_all_anchors_impl(&mut self.glyphs);
45    }
46
47    /// returns a list of all glyphs, sorted by component depth.
48    ///
49    /// This is also used for bracket layers.
50    ///
51    /// That is: a glyph in the list will always occur before any other glyph that
52    /// references it as a component.
53    pub(crate) fn depth_sorted_composite_glyphs(&self) -> Vec<SmolStr> {
54        fontdrasil::util::depth_sorted_composite_glyphs(&self.glyphs)
55    }
56}
57
58// the actual implementation: it's easier to test a free fn
59fn propagate_all_anchors_impl(glyphs: &mut BTreeMap<SmolStr, Glyph>) {
60    // the reference implementation does this recursively, but we opt to
61    // implement it by pre-sorting the work to ensure we always process components
62    // first.
63    let todo = fontdrasil::util::depth_sorted_composite_glyphs(glyphs);
64    let mut num_base_glyphs = HashMap::new();
65    // NOTE: there's an important detail here, which is that we need to call the
66    // 'anchors_traversing_components' function on each glyph, and save the returned
67    // anchors, but we only *set* those anchors on glyphs that have components.
68    // to make this work, we write the anchors to a separate data structure, and
69    // then only update the actual glyphs after we've done all the work.
70    let mut all_anchors = HashMap::new();
71    for name in todo {
72        let glyph = glyphs.get(&name).unwrap();
73        for layer in glyph.layers.iter().chain(glyph.bracket_layers.iter()) {
74            let anchors =
75                anchors_traversing_components(glyph, layer, &all_anchors, &mut num_base_glyphs);
76            maybe_log_new_anchors(&anchors, glyph, layer);
77            // if this is a bracket layer we use the actual axis values as the key,
78            // since it's possible that layers with the same axis values do not share
79            // the same layer id.
80            let layer_id = layer
81                .axis_rules_key()
82                .unwrap_or_else(|| layer.layer_id.clone());
83            all_anchors
84                .entry(name.clone())
85                .or_default()
86                .insert(layer_id, anchors);
87        }
88    }
89
90    // finally update our glyphs with the new anchors, where appropriate
91    for (name, layer_anchors) in all_anchors {
92        let glyph = glyphs.get_mut(&name).unwrap();
93        if glyph.has_components() {
94            for layer in &mut glyph.layers {
95                if let Some(new_anchors) = layer_anchors.get(&layer.layer_id) {
96                    layer.anchors = new_anchors.clone();
97                }
98            }
99
100            for bracket in &mut glyph.bracket_layers {
101                if let Some(new_anchors) = bracket
102                    .axis_rules_key()
103                    .and_then(|id| layer_anchors.get(&id))
104                    .or_else(|| layer_anchors.get(bracket.master_id()))
105                {
106                    bracket.anchors = new_anchors.clone();
107                }
108            }
109        }
110    }
111}
112
113fn maybe_log_new_anchors(anchors: &[Anchor], glyph: &Glyph, layer: &Layer) {
114    if !glyph.has_components() || !log::log_enabled!(log::Level::Trace) || anchors == layer.anchors
115    {
116        return;
117    }
118    let prev_names: Vec<_> = layer.anchors.iter().map(|a| &a.name).collect();
119    let new_names: Vec<_> = anchors.iter().map(|a| &a.name).collect();
120    log::trace!(
121        "propagated anchors for ('{}': {prev_names:?} -> {new_names:?}",
122        glyph.name,
123    );
124}
125
126/// Return the anchors for this glyph, including anchors from components
127///
128/// This function is a reimplmentation of a similarly named function in glyphs.app.
129///
130/// The logic for copying anchors from components into their containing composites
131/// is tricky. Anchors need to be adjusted in various ways:
132///
133/// - a speical "*origin" anchor may exist, which modifies the position of other anchors
134/// - if a component is flipped on the x or y axes, we rename "top" to "bottom"
135///   and/or "left" to "right"
136/// - we need to apply the transform from the component
137/// - we may need to rename an anchor when the component is part of a ligature glyph
138fn anchors_traversing_components<'a>(
139    glyph: &'a Glyph,
140    layer: &'a Layer,
141    // map of glyph -> anchors, per-layer, updated as we do each glyph;
142    // since we sort by component depth before doing work, we know that any components
143    // of the current glyph have been done first.
144    done_anchors: &HashMap<SmolStr, HashMap<String, Vec<Anchor>>>,
145    // each (glyph, layer) writes its number of base glyphs into this map during traversal
146    base_glyph_counts: &mut HashMap<(SmolStr, &'a str), usize>,
147) -> Vec<Anchor> {
148    if layer.anchors.is_empty() && layer.components().count() == 0 {
149        return Vec::new();
150    }
151
152    // if this is a mark and it has anchors, just return them
153    // (as in, don't even look at the components)
154    if !layer.anchors.is_empty() && glyph.category == Some(Category::Mark) {
155        return origin_adjusted_anchors(&layer.anchors).collect();
156    }
157
158    let is_ligature = glyph.sub_category == Some(Subcategory::Ligature);
159    let mut has_underscore = layer
160        .anchors
161        .iter()
162        .any(|anchor| anchor.name.starts_with('_'));
163
164    let mut number_of_base_glyphs = 0usize;
165    // we use an index map so we get the same ordering behaviour as python
166    let mut all_anchors = IndexMap::new();
167    for (component_idx, component) in layer.components().enumerate() {
168        // because we process dependencies first we know that all components
169        // referenced have already been propagated
170        let Some(mut anchors) =
171            // equivalent to the recursive call in the reference impl
172            get_component_layer_anchors(component, layer,  done_anchors)
173        else {
174            log::warn!(
175                "could not get layer '{}' for component '{}' of glyph '{}'",
176                layer.layer_id,
177                component.name,
178                glyph.name
179            );
180            continue;
181        };
182
183        // if this component has an explicitly set attachment anchor, use it
184        if let Some(comp_anchor) = component.anchor.as_ref().filter(|_| component_idx > 0) {
185            maybe_rename_component_anchor(comp_anchor.to_owned(), &mut anchors);
186        }
187
188        let component_number_of_base_glyphs = base_glyph_counts
189            .get(&(component.name.clone(), layer.layer_id.as_str()))
190            .copied()
191            .unwrap_or(0);
192
193        let comb_has_underscore = anchors
194            .iter()
195            .any(|a| a.name.len() >= 2 && a.name.starts_with('_'));
196        let comb_has_exit = anchors.iter().any(|a| a.name.ends_with("exit"));
197        if !(comb_has_underscore | comb_has_exit) {
198            // delete exit anchors we may have taken from earlier components
199            // (since a glyph should only have one exit anchor, and logically its at the end)
200            all_anchors.retain(|name: &SmolStr, _| !name.ends_with("exit"));
201        }
202
203        let scale = get_xy_rotation(component.transform);
204        for mut anchor in anchors {
205            let new_has_underscore = anchor.name.starts_with('_');
206            if (component_idx > 0 || has_underscore) && new_has_underscore {
207                continue;
208            }
209            // skip entry anchors on non-first glyphs
210            if component_idx > 0 && anchor.name.ends_with("entry") {
211                continue;
212            }
213
214            let mut new_anchor_name = rename_anchor_for_scale(&anchor.name, scale);
215            if is_ligature
216                && component_number_of_base_glyphs > 0
217                && !new_has_underscore
218                && !(new_anchor_name.ends_with("exit") || new_anchor_name.ends_with("entry"))
219            {
220                // dealing with marks like top_1 on a ligature
221                new_anchor_name = make_liga_anchor_name(new_anchor_name, number_of_base_glyphs);
222            }
223
224            apply_transform_to_anchor(&mut anchor, component.transform);
225            anchor.name = new_anchor_name;
226            all_anchors.insert(anchor.name.clone(), anchor);
227            has_underscore |= new_has_underscore;
228        }
229        number_of_base_glyphs += base_glyph_counts
230            .get(&(component.name.clone(), layer.layer_id.as_str()))
231            .copied()
232            .unwrap_or(0);
233    }
234
235    // now we've handled all the anchors from components, so copy over anchors
236    // that were explicitly defined on this layer:
237    all_anchors.extend(origin_adjusted_anchors(&layer.anchors).map(|a| (a.name.clone(), a)));
238    let mut has_underscore_anchor = false;
239    let mut has_mark_anchor = false;
240    let mut component_count_from_anchors = 0;
241
242    // now we count how many components we have, based on our anchors
243    for name in all_anchors.keys() {
244        has_underscore_anchor |= name.starts_with('_');
245        has_mark_anchor |= name.chars().next().unwrap_or('\0').is_ascii_alphabetic();
246        if !is_ligature
247            && number_of_base_glyphs == 0
248            && !name.starts_with('_')
249            && !(name.ends_with("entry") | name.ends_with("exit"))
250            && name.contains('_')
251        {
252            let (_, suffix) = name.split_once('_').unwrap();
253            // carets count space between components, so the last caret
254            // is n_components - 1
255            let maybe_add_one = if name.starts_with("caret") { 1 } else { 0 };
256            let anchor_index = suffix.parse::<usize>().unwrap_or(0) + maybe_add_one;
257            component_count_from_anchors = component_count_from_anchors.max(anchor_index);
258        }
259    }
260    if !has_underscore_anchor && number_of_base_glyphs == 0 && has_mark_anchor {
261        number_of_base_glyphs += 1;
262    }
263    number_of_base_glyphs = number_of_base_glyphs.max(component_count_from_anchors);
264
265    if layer.anchors.iter().any(|a| a.name == "_bottom") {
266        all_anchors.shift_remove("top");
267        all_anchors.shift_remove("_top");
268    }
269    if layer.anchors.iter().any(|a| a.name == "_top") {
270        all_anchors.shift_remove("bottom");
271        all_anchors.shift_remove("_bottom");
272    }
273    base_glyph_counts.insert(
274        (glyph.name.clone(), layer.layer_id.as_str()),
275        number_of_base_glyphs,
276    );
277    all_anchors.into_values().collect()
278}
279
280/// returns an iterator over anchors in the layer, accounting for a possible "*origin" anchor
281///
282/// If that anchor is present it will be used to adjust the positions of other
283/// anchors, and will not be included in the output.
284fn origin_adjusted_anchors(anchors: &[Anchor]) -> impl Iterator<Item = Anchor> + '_ {
285    let origin = anchors
286        .iter()
287        .find_map(Anchor::origin_delta)
288        .unwrap_or_default();
289    anchors
290        .iter()
291        .filter(|a| !a.is_origin())
292        .cloned()
293        .map(move |mut a| {
294            a.pos -= origin;
295            a
296        })
297}
298
299// returns a vec2 where for each axis a negative value indicates that axis is considerd flipped
300fn get_xy_rotation(xform: Affine) -> Vec2 {
301    // this is based on examining the behaviour of glyphs via the macro panel
302    // and careful testing.
303    let [xx, xy, ..] = xform.as_coeffs();
304    // first take the rotation
305    let angle = xy.atan2(xx);
306    // then remove the rotation, and take the scale
307    let rotated = xform.pre_rotate(-angle).as_coeffs();
308    let mut scale = Vec2::new(rotated[0], rotated[3]);
309    // then invert the scale if the rotation was >= 180°
310    if (angle.to_degrees() - 180.0).abs() < 0.001 {
311        scale *= -1.0;
312    }
313
314    scale
315}
316
317// apply the transform but also do some rounding, so we don't have anchors
318// with points like (512, 302.000000006)
319fn apply_transform_to_anchor(anchor: &mut Anchor, transform: Affine) {
320    // how many zeros do we care about? not this many
321    const ROUND_TO: f64 = 1e6;
322    let mut pos = (transform * anchor.pos).to_vec2();
323    pos *= ROUND_TO;
324    pos = pos.round();
325    pos /= ROUND_TO;
326    anchor.pos = pos.to_point();
327}
328
329fn maybe_rename_component_anchor(comp_name: SmolStr, anchors: &mut [Anchor]) {
330    // e.g, go from 'top' to 'top_1'
331    let Some((sub_name, _)) = comp_name.as_str().split_once('_') else {
332        return;
333    };
334    let mark_name = format_smolstr!("_{sub_name}");
335    if anchors.iter().any(|a| a.name == sub_name) && anchors.iter().any(|a| a.name == mark_name) {
336        anchors
337            .iter_mut()
338            .find(|a| a.name == sub_name)
339            .unwrap()
340            .name = comp_name.clone();
341    }
342}
343
344fn make_liga_anchor_name(name: SmolStr, base_number: usize) -> SmolStr {
345    match name.split_once('_') {
346        // if this anchor already has a number (like 'top_2') we want to consider that
347        Some((name, suffix)) => {
348            let suffix = base_number + suffix.parse::<usize>().ok().unwrap_or(1);
349            format_smolstr!("{name}_{suffix}")
350        }
351        // otherwise we're turning 'top' into 'top_N'
352        None => format_smolstr!("{name}_{}", base_number + 1),
353    }
354}
355
356// if a component is rotated, flip bottom/top, left/right, entry/exit
357fn rename_anchor_for_scale(name: &SmolStr, scale: Vec2) -> SmolStr {
358    // swap the two words in the target, if they're present
359    fn swap_pair(s: &mut String, one: &str, two: &str) {
360        fn replace(s: &mut String, target: &str, by: &str) -> bool {
361            if let Some(idx) = s.find(target) {
362                s.replace_range(idx..idx + target.len(), by);
363                return true;
364            }
365            false
366        }
367        // once we swap 'left' for 'right' we don't want to then check for 'right'!
368        if !replace(s, one, two) {
369            replace(s, two, one);
370        }
371    }
372
373    if scale.x >= 0. && scale.y >= 0. {
374        return name.to_owned();
375    }
376
377    let mut name = name.to_string();
378    if scale.y < 0. {
379        swap_pair(&mut name, "bottom", "top");
380    }
381    if scale.x < 0. {
382        swap_pair(&mut name, "left", "right");
383        swap_pair(&mut name, "exit", "entry");
384    }
385
386    SmolStr::from(name)
387}
388
389// in glyphs.app this function will synthesize a layer if it is missing.
390fn get_component_layer_anchors(
391    component: &Component,
392    layer: &Layer,
393    anchors: &HashMap<SmolStr, HashMap<String, Vec<Anchor>>>,
394) -> Option<Vec<Anchor>> {
395    let anchors = anchors.get(&component.name)?;
396    if let Some(id) = layer.axis_rules_key() {
397        anchors.get(&id).or_else(|| anchors.get(layer.master_id()))
398    } else {
399        anchors.get(&layer.layer_id)
400    }
401    .cloned()
402}
403
404#[cfg(test)]
405mod tests {
406
407    use std::collections::BTreeSet;
408
409    use kurbo::Point;
410
411    use crate::{
412        Layer, Shape,
413        font::{AxisRule, LayerAttributes},
414        glyphdata::GlyphData,
415    };
416
417    use super::*;
418
419    #[derive(Debug, Default)]
420    struct GlyphSetBuilder(BTreeMap<SmolStr, Glyph>);
421
422    impl GlyphSetBuilder {
423        fn new() -> Self {
424            Default::default()
425        }
426
427        fn build(&self) -> BTreeMap<SmolStr, Glyph> {
428            self.0.clone()
429        }
430
431        fn add_glyph(&mut self, name: &str, build_fn: impl FnOnce(&mut GlyphBuilder)) -> &mut Self {
432            let mut glyph = GlyphBuilder::new(name);
433            build_fn(&mut glyph);
434            self.0.insert(glyph.glyph.name.clone(), glyph.build());
435            self
436        }
437    }
438    // a little helper to make it easier to generate data for these tests
439    #[derive(Debug, Default)]
440    struct GlyphBuilder {
441        glyph: Glyph,
442        last_layer_is_bracket: bool,
443    }
444
445    impl GlyphBuilder {
446        fn new(name: &str) -> Self {
447            let mut this = GlyphBuilder {
448                glyph: Glyph {
449                    name: name.into(),
450                    export: true,
451                    ..Default::default()
452                },
453                last_layer_is_bracket: false,
454            };
455            if let Some(result) = GlyphData::default().query(name, None) {
456                this.set_category(result.category);
457                if let Some(subcategory) = result.subcategory {
458                    this.set_subcategory(subcategory);
459                }
460                if let Some(unicode) = result.codepoint {
461                    this.set_unicode(unicode);
462                }
463            }
464            this.add_layer();
465            this
466        }
467
468        fn build(&self) -> Glyph {
469            self.glyph.clone()
470        }
471
472        /// Add a new layer to a glyph; all other operations work on the last added layer
473        fn add_layer(&mut self) -> &mut Self {
474            self.glyph.layers.push(Layer {
475                layer_id: format!("master-{}", self.glyph.layers.len()),
476                ..Default::default()
477            });
478            self.last_layer_is_bracket = false;
479            self
480        }
481
482        fn add_bracket_layer(&mut self, axis_rules: Vec<(Option<i64>, Option<i64>)>) -> &mut Self {
483            let assoc_master_id = self.glyph.layers.last().unwrap().layer_id.clone();
484            self.glyph.bracket_layers.push({
485                Layer {
486                    layer_id: format!("bracket-{}", self.glyph.bracket_layers.len()),
487                    associated_master_id: Some(assoc_master_id),
488                    attributes: LayerAttributes {
489                        axis_rules: axis_rules
490                            .into_iter()
491                            .map(|(min, max)| AxisRule { min, max })
492                            .collect(),
493                        ..Default::default()
494                    },
495                    ..Default::default()
496                }
497            });
498            self.last_layer_is_bracket = true;
499            self
500        }
501
502        fn last_layer_mut(&mut self) -> &mut Layer {
503            if self.last_layer_is_bracket {
504                self.glyph.bracket_layers.last_mut().unwrap()
505            } else {
506                self.glyph.layers.last_mut().unwrap()
507            }
508        }
509
510        fn set_unicode(&mut self, unicode: u32) -> &mut Self {
511            self.glyph.unicode = BTreeSet::from([unicode]);
512            self
513        }
514
515        fn set_category(&mut self, category: Category) -> &mut Self {
516            self.glyph.category = Some(category);
517            self
518        }
519
520        fn set_subcategory(&mut self, sub_category: Subcategory) -> &mut Self {
521            self.glyph.sub_category = Some(sub_category);
522            self
523        }
524
525        // use an int for pos to simplify the call site ('0' instead of'0.0')
526        fn add_component(&mut self, name: &str, pos: (i32, i32)) -> &mut Self {
527            self.last_layer_mut()
528                .shapes
529                .push(Shape::Component(Component {
530                    name: name.into(),
531                    transform: Affine::translate((pos.0 as f64, pos.1 as f64)),
532                    ..Default::default()
533                }));
534            self
535        }
536
537        /// Set an explicit translate + rotation for the component
538        fn rotate_component(&mut self, degrees: f64) -> &mut Self {
539            if let Some(Shape::Component(comp)) = self.last_layer_mut().shapes.last_mut() {
540                comp.transform = comp.transform.pre_rotate(degrees.to_radians());
541            }
542            self
543        }
544
545        /// add an explicit anchor to the last added component
546        fn add_component_anchor(&mut self, name: &str) -> &mut Self {
547            if let Some(Shape::Component(comp)) = self.last_layer_mut().shapes.last_mut() {
548                comp.anchor = Some(name.into());
549            }
550            self
551        }
552        fn add_anchor(&mut self, name: &str, pos: (i32, i32)) -> &mut Self {
553            self.last_layer_mut().anchors.push(Anchor {
554                name: name.into(),
555                pos: Point::new(pos.0 as _, pos.1 as _),
556            });
557            self
558        }
559    }
560
561    impl PartialEq<(&str, (f64, f64))> for Anchor {
562        fn eq(&self, other: &(&str, (f64, f64))) -> bool {
563            self.name == other.0 && self.pos == other.1.into()
564        }
565    }
566
567    #[test]
568    fn no_components_anchors_are_unchanged() {
569        // derived from the observed behaviour of glyphs 3.2.2 (3259)
570        let glyphs = GlyphSetBuilder::new()
571            .add_glyph("A", |glyph| {
572                glyph
573                    .add_anchor("bottom", (234, 0))
574                    .add_anchor("ogonek", (411, 0))
575                    .add_anchor("top", (234, 810));
576            })
577            .add_glyph("acutecomb", |glyph| {
578                glyph
579                    .add_anchor("_top", (0, 578))
580                    .add_anchor("top", (0, 810));
581            })
582            .build();
583
584        let mut glyphs_2 = glyphs.clone();
585        propagate_all_anchors_impl(&mut glyphs_2);
586        assert_eq!(glyphs, glyphs_2, "nothing should change here");
587    }
588
589    #[test]
590    fn basic_composite_anchor() {
591        // derived from the observed behaviour of glyphs 3.2.2 (3259)
592        let mut glyphs = GlyphSetBuilder::new()
593            .add_glyph("A", |glyph| {
594                glyph
595                    .add_anchor("bottom", (234, 0))
596                    .add_anchor("ogonek", (411, 0))
597                    .add_anchor("top", (234, 810));
598            })
599            .add_glyph("acutecomb", |glyph| {
600                glyph
601                    .add_anchor("_top", (0, 578))
602                    .add_anchor("top", (0, 810));
603            })
604            .add_glyph("Aacute", |glyph| {
605                glyph
606                    .add_component("A", (0, 0))
607                    .add_component("acutecomb", (234, 232));
608            })
609            .build();
610        propagate_all_anchors_impl(&mut glyphs);
611
612        let new_glyph = glyphs.get("Aacute").unwrap();
613        assert_eq!(
614            new_glyph.layers[0].anchors,
615            [
616                ("bottom", (234., 0.)),
617                ("ogonek", (411., 0.)),
618                ("top", (234., 1042.))
619            ]
620        );
621    }
622
623    #[test]
624    fn propagate_ligature_anchors() {
625        // derived from the observed behaviour of glyphs 3.2.2 (3259)
626        // this is based on the IJ glyph in Oswald (ExtraLight)
627        let mut glyphs = GlyphSetBuilder::new()
628            .add_glyph("I", |glyph| {
629                glyph
630                    .add_anchor("bottom", (103, 0))
631                    .add_anchor("ogonek", (103, 0))
632                    .add_anchor("top", (103, 810))
633                    .add_anchor("topleft", (20, 810));
634            })
635            .add_glyph("J", |glyph| {
636                glyph
637                    .add_anchor("bottom", (133, 0))
638                    .add_anchor("top", (163, 810));
639            })
640            .add_glyph("IJ", |glyph| {
641                glyph
642                    // we need to manually override this, it isn't actually a
643                    // ligature by default
644                    .set_subcategory(Subcategory::Ligature)
645                    .add_component("I", (0, 0))
646                    .add_component("J", (206, 0));
647            })
648            .build();
649        propagate_all_anchors_impl(&mut glyphs);
650        let ij = glyphs.get("IJ").unwrap();
651        // these were derived by running the built in glyphs.app propagate anchors
652        // method from the macro panel
653        assert_eq!(
654            ij.layers[0].anchors,
655            [
656                ("bottom_1", (103., 0.)),
657                ("ogonek_1", (103., 0.)),
658                ("top_1", (103., 810.)),
659                ("topleft_1", (20., 810.)),
660                ("bottom_2", (339., 0.)),
661                ("top_2", (369., 810.))
662            ]
663        )
664    }
665
666    #[test]
667    fn digraphs_arent_ligatures() {
668        // derived from the observed behaviour of glyphs 3.2.2 (3259)
669        // this is based on the IJ glyph in Oswald (ExtraLight)
670        let mut glyphs = GlyphSetBuilder::new()
671            .add_glyph("I", |glyph| {
672                glyph
673                    .add_anchor("bottom", (103, 0))
674                    .add_anchor("ogonek", (103, 0))
675                    .add_anchor("top", (103, 810))
676                    .add_anchor("topleft", (20, 810));
677            })
678            .add_glyph("J", |glyph| {
679                glyph
680                    .add_anchor("bottom", (133, 0))
681                    .add_anchor("top", (163, 810));
682            })
683            .add_glyph("IJ", |glyph| {
684                glyph
685                    .add_component("I", (0, 0))
686                    .add_component("J", (206, 0));
687            })
688            .build();
689        propagate_all_anchors_impl(&mut glyphs);
690        let ij = glyphs.get("IJ").unwrap();
691        // these were derived by running the built in glyphs.app propagate anchors
692        // method from the macro panel
693        assert_eq!(
694            ij.layers[0].anchors,
695            [
696                ("bottom", (339., 0.)),
697                ("ogonek", (103., 0.)),
698                ("top", (369., 810.)),
699                ("topleft", (20., 810.)),
700            ]
701        )
702    }
703
704    #[test]
705    fn propagate_across_layers() {
706        // derived from the observed behaviour of glyphs 3.2.2 (3259)
707        let mut glyphs = GlyphSetBuilder::new()
708            .add_glyph("A", |glyph| {
709                glyph
710                    .add_anchor("bottom", (290, 10))
711                    .add_anchor("ogonek", (490, 3))
712                    .add_anchor("top", (290, 690))
713                    .add_layer()
714                    .add_anchor("bottom", (300, 0))
715                    .add_anchor("ogonek", (540, 10))
716                    .add_anchor("top", (300, 700));
717            })
718            .add_glyph("acutecomb", |glyph| {
719                glyph
720                    .add_anchor("_top", (335, 502))
721                    .add_anchor("top", (353, 721))
722                    .add_layer()
723                    .add_anchor("_top", (366, 500))
724                    .add_anchor("top", (366, 765));
725            })
726            .add_glyph("Aacute", |glyph| {
727                glyph
728                    .add_component("A", (0, 0))
729                    .add_component("acutecomb", (-45, 188))
730                    .add_layer()
731                    .add_component("A", (0, 0))
732                    .add_component("acutecomb", (-66, 200));
733            })
734            .build();
735        propagate_all_anchors_impl(&mut glyphs);
736
737        let new_glyph = glyphs.get("Aacute").unwrap();
738        assert_eq!(
739            new_glyph.layers[0].anchors,
740            [
741                ("bottom", (290., 10.)),
742                ("ogonek", (490., 3.)),
743                ("top", (308., 909.))
744            ]
745        );
746
747        assert_eq!(
748            new_glyph.layers[1].anchors,
749            [
750                ("bottom", (300., 0.)),
751                ("ogonek", (540., 10.)),
752                ("top", (300., 965.))
753            ]
754        );
755    }
756
757    // adapted from https://github.com/googlefonts/glyphsLib/blob/f81c00c74/tests/builder/transformations/propagate_anchors_test.py#L384
758    // which was itself adapted from our test, above, but more is better?
759    #[test]
760    fn propagate_across_layers_including_bracket_layers() {
761        let _ = env_logger::builder().is_test(true).try_init();
762        let mut glyphs = GlyphSetBuilder::new()
763            .add_glyph("A", |glyph| {
764                glyph
765                    .add_anchor("bottom", (206, 16))
766                    .add_anchor("ogonek", (360, 13))
767                    .add_anchor("top", (212, 724))
768                    .add_bracket_layer(vec![(Some(500), None)])
769                    .add_anchor("bottom", (206, 16))
770                    .add_anchor("top", (212, 724))
771                    .add_layer()
772                    .add_anchor("bottom", (278, 12))
773                    .add_anchor("ogonek", (464, 13))
774                    .add_anchor("top", (281, 758))
775                    .add_bracket_layer(vec![(Some(500), None)])
776                    .add_anchor("bottom", (278, 12))
777                    .add_anchor("top", (281, 758));
778                //.add_backup_layer()
779                //.add_anchor("top", (290, 690))
780            })
781            .add_glyph("acutecomb", |glyph| {
782                glyph
783                    .add_anchor("_top", (150, 580))
784                    .add_anchor("top", (170, 792))
785                    .add_layer()
786                    .add_anchor("_top", (167, 580))
787                    .add_anchor("top", (170, 792));
788                //.add_backup_layer()
789                //.add_anchor("_top", (335, 502));
790            })
791            .add_glyph("Aacute", |glyph| {
792                glyph
793                    .add_component("A", (0, 0))
794                    .add_component("acutecomb", (62, 144))
795                    .add_bracket_layer(vec![(Some(500), None)])
796                    .add_component("A", (20, 0))
797                    .add_component("acutecomb", (82, 144))
798                    .add_layer()
799                    .add_component("A", (0, 0))
800                    .add_component("acutecomb", (114, 178))
801                    .add_bracket_layer(vec![(Some(500), None)])
802                    .add_component("A", (30, 0))
803                    .add_component("acutecomb", (144, 178));
804                //.add_backup_layer()
805                //.add_component("A", (0, 0))
806                //.add_component("acutecomb", (-45, 188))
807            })
808            .build();
809
810        propagate_all_anchors_impl(&mut glyphs);
811
812        let new_glyph = glyphs.get("Aacute").unwrap();
813        assert_eq!(new_glyph.layers[0].layer_id, "master-0");
814        assert_eq!(
815            new_glyph.layers[0].anchors,
816            [
817                ("bottom", (206., 16.)),
818                ("ogonek", (360., 13.)),
819                ("top", (232., 936.))
820            ]
821        );
822
823        assert_eq!(new_glyph.layers[1].layer_id, "master-1");
824        assert_eq!(
825            new_glyph.layers[1].anchors,
826            [
827                ("bottom", (278., 12.)),
828                ("ogonek", (464., 13.)),
829                ("top", (284., 970.))
830            ]
831        );
832
833        // and bracket layers too??
834        assert_eq!(new_glyph.bracket_layers.len(), 2);
835        assert_eq!(
836            new_glyph.bracket_layers[0].anchors,
837            [
838                // from bracket layer on A, with xform from transform on bracket component
839                ("bottom", (226., 16.)),
840                // from default master on 'acute', shifted by xform
841                ("top", (252., 936.))
842            ]
843        );
844        assert_eq!(
845            new_glyph.bracket_layers[1].anchors,
846            [("bottom", (308., 12.)), ("top", (314., 970.))]
847        );
848    }
849
850    #[test]
851    fn remove_exit_anchor_on_component() {
852        // derived from the observed behaviour of glyphs 3.2.2 (3259)
853        let mut glyphs = GlyphSetBuilder::new()
854            .add_glyph("comma", |_| {})
855            .add_glyph("ain-ar.init", |glyph| {
856                glyph
857                    .add_anchor("top", (294, 514))
858                    .add_anchor("exit", (0, 0));
859            })
860            .add_glyph("ain-ar.init.alt", |glyph| {
861                glyph
862                    .add_component("ain-ar.init", (0, 0))
863                    .add_component("comma", (0, 0));
864            })
865            .build();
866        propagate_all_anchors_impl(&mut glyphs);
867
868        let new_glyph = glyphs.get("ain-ar.init.alt").unwrap();
869        assert_eq!(new_glyph.layers[0].anchors, [("top", (294., 514.)),]);
870    }
871
872    #[test]
873    fn component_anchor() {
874        // derived from the observed behaviour of glyphs 3.2.2 (3259)
875        let mut glyphs = GlyphSetBuilder::new()
876            .add_glyph("acutecomb", |glyph| {
877                glyph
878                    .add_anchor("_top", (150, 580))
879                    .add_anchor("top", (170, 792));
880            })
881            .add_glyph("aa", |glyph| {
882                glyph
883                    .add_anchor("bottom_1", (218, 8))
884                    .add_anchor("bottom_2", (742, 7))
885                    .add_anchor("ogonek_1", (398, 9))
886                    .add_anchor("ogonek_2", (902, 9))
887                    .add_anchor("top_1", (227, 548))
888                    .add_anchor("top_2", (746, 548));
889            })
890            .add_glyph("a_a", |glyph| {
891                glyph.add_component("aa", (0, 0));
892            })
893            .add_glyph("a_aacute", |glyph| {
894                glyph
895                    .add_component("a_a", (0, 0))
896                    .add_component("acutecomb", (596, -32))
897                    .add_component_anchor("top_2");
898            })
899            .build();
900        propagate_all_anchors_impl(&mut glyphs);
901
902        let new_glyph = glyphs.get("a_aacute").unwrap();
903        assert_eq!(
904            new_glyph.layers[0].anchors,
905            [
906                ("bottom_1", (218., 8.)),
907                ("bottom_2", (742., 7.)),
908                ("ogonek_1", (398., 9.)),
909                ("ogonek_2", (902., 9.)),
910                ("top_1", (227., 548.)),
911                ("top_2", (766., 760.)),
912            ]
913        );
914    }
915
916    #[test]
917    fn origin_anchor() {
918        // derived from the observed behaviour of glyphs 3.2.2 (3259)
919        let mut glyphs = GlyphSetBuilder::new()
920            .add_glyph("a", |glyph| {
921                glyph
922                    .add_anchor("*origin", (-20, 0))
923                    .add_anchor("bottom", (242, 7))
924                    .add_anchor("ogonek", (402, 9))
925                    .add_anchor("top", (246, 548));
926            })
927            .add_glyph("acutecomb", |glyph| {
928                glyph
929                    .add_anchor("_top", (150, 580))
930                    .add_anchor("top", (170, 792));
931            })
932            .add_glyph("aacute", |glyph| {
933                glyph
934                    .add_component("a", (0, 0))
935                    .add_component("acutecomb", (116, -32));
936            })
937            .build();
938        propagate_all_anchors_impl(&mut glyphs);
939
940        let new_glyph = glyphs.get("aacute").unwrap();
941        assert_eq!(
942            new_glyph.layers[0].anchors,
943            [
944                ("bottom", (262.0, 7.0)),
945                ("ogonek", (422.0, 9.0)),
946                ("top", (286.0, 760.0)),
947            ]
948        );
949    }
950
951    #[test]
952    fn invert_names_on_rotation() {
953        // derived from the observed behaviour of glyphs 3.2.2 (3259)
954        let mut glyphs = GlyphSetBuilder::new()
955            .add_glyph("comma", |_| {})
956            .add_glyph("commaaccentcomb", |glyph| {
957                glyph
958                    .add_anchor("_bottom", (289, 0))
959                    .add_anchor("mybottom", (277, -308))
960                    .add_component("comma", (9, -164));
961            })
962            .add_glyph("commaturnedabovecomb", |glyph| {
963                glyph
964                    .add_component("commaaccentcomb", (589, 502))
965                    .rotate_component(180.);
966            })
967            .build();
968        propagate_all_anchors_impl(&mut glyphs);
969
970        let new_glyph = glyphs.get("commaturnedabovecomb").unwrap();
971        assert_eq!(
972            new_glyph.layers[0].anchors,
973            [("_top", (300., 502.)), ("mytop", (312., 810.)),]
974        );
975    }
976
977    #[test]
978    fn affine_scale() {
979        let affine = Affine::rotate((180.0f64).to_radians()).then_translate((589., 502.).into());
980        let delta = get_xy_rotation(affine);
981        assert!(delta.x.is_sign_negative() && delta.y.is_sign_negative());
982
983        let affine = Affine::translate((10., 10.));
984        let delta = get_xy_rotation(affine);
985        assert!(delta.x.is_sign_positive() && delta.y.is_sign_positive());
986        let flip_y = get_xy_rotation(Affine::FLIP_Y);
987        assert!(flip_y.y.is_sign_negative());
988        assert!(flip_y.x.is_sign_positive());
989        let flip_x = get_xy_rotation(Affine::FLIP_X);
990        assert!(flip_x.y.is_sign_positive());
991        assert!(flip_x.x.is_sign_negative());
992
993        let rotate_flip = Affine::rotate((180.0f64).to_radians())
994            .then_translate((589., 502.).into())
995            * Affine::FLIP_X;
996        let rotate_flip = get_xy_rotation(rotate_flip);
997        assert!(rotate_flip.x.is_sign_positive());
998        assert!(rotate_flip.y.is_sign_negative());
999    }
1000
1001    // the tricky parts of these files have been factored out into separate tests,
1002    // but we'll keep them in case there are other regressions lurking
1003    #[test]
1004    fn real_files() {
1005        let expected =
1006            Font::load_raw("../resources/testdata/glyphs3/PropagateAnchorsTest-propagated.glyphs")
1007                .unwrap();
1008        let font = Font::load(std::path::Path::new(
1009            "../resources/testdata/glyphs3/PropagateAnchorsTest.glyphs",
1010        ))
1011        .unwrap();
1012
1013        assert_eq!(expected.glyphs.len(), font.glyphs.len());
1014        assert!(
1015            expected
1016                .glyphs
1017                .keys()
1018                .zip(font.glyphs.keys())
1019                .all(|(a, b)| a == b)
1020        );
1021
1022        for (g1, g2) in expected.glyphs.values().zip(font.glyphs.values()) {
1023            assert_eq!(g1.layers.len(), g2.layers.len());
1024            for (l1, l2) in g1.layers.iter().zip(g2.layers.iter()) {
1025                let a1 = l1.anchors.clone();
1026                let a2 = l2.anchors.clone();
1027                assert_eq!(a1, a2, "{}", g1.name);
1028            }
1029        }
1030    }
1031
1032    #[test]
1033    fn dont_propagate_anchors() {
1034        let font = Font::load(std::path::Path::new(
1035            "../resources/testdata/glyphs2/DontPropagateAnchors.glyphs",
1036        ))
1037        .unwrap();
1038        assert_eq!(font.custom_parameters.propagate_anchors, Some(false));
1039        let glyph = font.glyphs.get("Aacute").unwrap();
1040        assert!(glyph.layers.first().unwrap().anchors.is_empty());
1041    }
1042}