Skip to main content

read_fonts/model/font/
instance.rs

1//! Font instance representation.
2
3use super::Font;
4use crate::{
5    tables::{
6        avar::Avar,
7        fvar::Fvar,
8        layout::{Condition, FeatureVariations},
9    },
10    TableProvider,
11};
12use alloc::vec::Vec;
13use core::{
14    str::FromStr,
15    sync::atomic::{self, AtomicU32},
16};
17use types::{Fixed, Tag};
18
19/// A specific instance of a font, with a size and variation settings.
20pub struct FontInstance {
21    font: Font,
22    size: Option<f32>,
23    coords: CoordStorage,
24    feature_vars: FeatureVarsStorage,
25}
26
27impl FontInstance {
28    /// Returns a builder for configuring a font instance from the given font.
29    pub fn builder(font: &Font) -> FontInstanceBuilder {
30        FontInstanceBuilder {
31            instance: Self {
32                font: font.clone(),
33                size: None,
34                coords: CoordStorage::default(),
35                feature_vars: FeatureVarsStorage::new(),
36            },
37        }
38    }
39
40    /// Returns the font for this instance.
41    pub fn font(&self) -> &Font {
42        &self.font
43    }
44
45    /// Returns the size of the font instance, in pixels per em.
46    pub fn size(&self) -> Option<f32> {
47        self.size
48    }
49
50    /// Returns the normalized variation coordinates for this font instance.
51    pub fn normalized_coords(&self) -> &[NormalizedCoord] {
52        self.coords.as_slice()
53    }
54
55    /// Returns the selected feature variations for this font instance.
56    pub fn feature_variations(&self) -> FontFeatureVariations {
57        self.feature_vars.load(&self.font, self.coords.as_slice())
58    }
59}
60
61impl core::ops::Deref for FontInstance {
62    type Target = Font;
63    fn deref(&self) -> &Font {
64        self.font()
65    }
66}
67
68/// Builder for configuring a font instance.
69pub struct FontInstanceBuilder {
70    instance: FontInstance,
71}
72
73impl FontInstanceBuilder {
74    /// Sets the size for the font instance, in pixels per em.
75    ///
76    /// Setting this to `None` disables scaling.
77    pub fn size(mut self, size: Option<f32>) -> Self {
78        self.instance.size = size;
79        self
80    }
81
82    /// Sets the variations for the font instance from an unordered sequence of
83    /// variations in user space.
84    ///
85    /// Omitted axes will be set to their default values. Unsupported axes are
86    /// ignored. If an axis is specified multiple times, the last value is used.
87    ///
88    /// This will overwrite any previous variation settings.
89    pub fn variations<V>(mut self, variations: V) -> Self
90    where
91        V: IntoIterator,
92        V::Item: Into<FontVariation>,
93    {
94        self.set_variations(variations);
95        self
96    }
97
98    /// Sets the variations for the font instance from an ordered sequence
99    /// of normalized coordinates.
100    ///
101    /// If the number of provided coordinates is less than the number of axes,
102    /// the remaining axes will be set to their default values. If the number
103    /// of provided coordinates is greater than the number of axes, the extra
104    /// coordinates will be ignored.
105    ///
106    /// This will overwrite any previous variation settings.
107    pub fn normalized_coords(mut self, coords: impl IntoIterator<Item = NormalizedCoord>) -> Self {
108        self.set_coords(coords);
109        self
110    }
111
112    /// Sets the variations for the font instance from a named instance.
113    ///
114    /// If the given named instance index is invalid, then variation settings
115    /// will be reset to default.
116    ///
117    /// This will overwrite any previous variation settings.
118    pub fn named_instance(mut self, index: usize) -> Self {
119        self.set_named_instance(index);
120        self
121    }
122
123    /// Sets the variations for the font instance from a named instance, with
124    /// additional overrides.
125    ///
126    /// If the given named instance index is invalid, then it is ignored and
127    /// only overrides are applied.
128    ///
129    /// This will overwrite any previous variation settings.
130    pub fn named_instance_with_overrides<V>(mut self, index: usize, overrides: V) -> Self
131    where
132        V: IntoIterator,
133        V::Item: Into<FontVariation>,
134    {
135        self.set_named_instance_with_overrides(index, overrides);
136        self
137    }
138
139    /// Builds the font instance.
140    pub fn build(self) -> FontInstance {
141        self.instance
142    }
143}
144
145impl FontInstanceBuilder {
146    fn set_variations<V>(&mut self, variations: V)
147    where
148        V: IntoIterator,
149        V::Item: Into<FontVariation>,
150    {
151        let tables = self.instance.font.tables();
152        if let Ok(fvar) = tables.fvar() {
153            set_variations(
154                &fvar,
155                tables.avar().ok(),
156                &mut self.instance.coords,
157                variations,
158            );
159        } else {
160            self.instance.coords.resize(0);
161        }
162    }
163
164    fn set_coords(&mut self, coords: impl IntoIterator<Item = NormalizedCoord>) {
165        if let Ok(fvar) = self.instance.font.tables().fvar() {
166            let count = fvar.axis_count() as usize;
167            self.instance.coords.resize(count);
168            for (dst, src) in self.instance.coords.as_mut_slice().iter_mut().zip(
169                coords
170                    .into_iter()
171                    .chain(core::iter::repeat(NormalizedCoord::ZERO)),
172            ) {
173                *dst = src;
174            }
175            self.instance.coords.clear_if_all_zeroes();
176        } else {
177            self.instance.coords.resize(0);
178        }
179    }
180
181    fn set_named_instance(&mut self, index: usize) {
182        let tables = self.instance.font.tables();
183        if let Ok(fvar) = tables.fvar() {
184            set_variations(
185                &fvar,
186                tables.avar().ok(),
187                &mut self.instance.coords,
188                named_instance_variations(&fvar, index),
189            );
190        } else {
191            self.instance.coords.resize(0);
192        }
193    }
194
195    fn set_named_instance_with_overrides<V>(&mut self, index: usize, overrides: V)
196    where
197        V: IntoIterator,
198        V::Item: Into<FontVariation>,
199    {
200        let tables = self.instance.font.tables();
201        if let Ok(fvar) = tables.fvar() {
202            set_variations(
203                &fvar,
204                tables.avar().ok(),
205                &mut self.instance.coords,
206                named_instance_variations(&fvar, index)
207                    .chain(overrides.into_iter().map(Into::into)),
208            );
209        } else {
210            self.instance.coords.resize(0);
211        }
212    }
213}
214
215// Helper to extract an iterator of FontVariation from a named instance index.
216fn named_instance_variations<'a>(
217    fvar: &'a Fvar,
218    index: usize,
219) -> impl Iterator<Item = FontVariation> + 'a {
220    fvar.axis_instance_arrays()
221        .ok()
222        .and_then(|arrays| {
223            let axes = arrays.axes();
224            arrays.instances().get(index).ok().map(|instance| {
225                axes.iter()
226                    .zip(instance.coordinates)
227                    .map(|(axis, coord)| FontVariation::new(axis.axis_tag(), coord.get().to_f32()))
228            })
229        })
230        .into_iter()
231        .flatten()
232}
233
234/// Helper for setting variations.
235///
236/// Pulled out into a separate function to avoid borrow checker issues.
237fn set_variations<V>(fvar: &Fvar, avar: Option<Avar>, coords: &mut CoordStorage, variations: V)
238where
239    V: IntoIterator,
240    V::Item: Into<FontVariation>,
241{
242    coords.resize(fvar.axis_count() as usize);
243    fvar.user_to_normalized(
244        avar.as_ref(),
245        variations
246            .into_iter()
247            .map(Into::into)
248            .map(|var| (var.tag, Fixed::from_f64(var.value as _))),
249        coords.as_mut_slice(),
250    );
251    coords.clear_if_all_zeroes();
252}
253
254/// A normalized variation coordinate in 2.14 fixed point in the range
255/// [-1.0, 1.0].
256pub type NormalizedCoord = types::F2Dot14;
257
258/// A variation setting for a font instance.
259///
260/// The tag identifies the axis, and the value is the desired value for that
261/// axis in user space.
262#[derive(Copy, Clone, PartialEq, Debug)]
263pub struct FontVariation {
264    /// The tag that identifies the axis.
265    pub tag: Tag,
266    /// The value for the axis in user space.
267    pub value: f32,
268}
269
270impl FontVariation {
271    /// Creates a new font variation with the given tag and value.
272    pub fn new(tag: Tag, value: f32) -> Self {
273        Self { tag, value }
274    }
275}
276
277// Various conversions for FontVariation that have proven to be ergonomically
278// useful in practice. These allow, for example, passing &[("wght", 700.0)]
279// directly to the variations() method of FontInstanceBuilder without needing
280//to manually construct FontVariation objects or tags.
281
282impl From<&'_ FontVariation> for FontVariation {
283    fn from(value: &'_ FontVariation) -> Self {
284        *value
285    }
286}
287
288impl From<(Tag, f32)> for FontVariation {
289    fn from(value: (Tag, f32)) -> Self {
290        Self::new(value.0, value.1)
291    }
292}
293
294impl From<&(Tag, f32)> for FontVariation {
295    fn from(value: &(Tag, f32)) -> Self {
296        Self::new(value.0, value.1)
297    }
298}
299
300impl From<(&str, f32)> for FontVariation {
301    fn from(value: (&str, f32)) -> Self {
302        Self::new(Tag::from_str(value.0).unwrap_or_default(), value.1)
303    }
304}
305
306impl From<&(&str, f32)> for FontVariation {
307    fn from(value: &(&str, f32)) -> Self {
308        Self::new(Tag::from_str(value.0).unwrap_or_default(), value.1)
309    }
310}
311
312/// Maximum number of coordinates we store inline. Chosen to maximize
313/// number of coords while minimizing space overhead.
314const MAX_INLINE_COORDS: usize = 15;
315
316enum CoordStorage {
317    None,
318    Inline([NormalizedCoord; MAX_INLINE_COORDS], u8),
319    Heap(Vec<NormalizedCoord>),
320}
321
322impl Default for CoordStorage {
323    fn default() -> Self {
324        Self::None
325    }
326}
327
328impl CoordStorage {
329    /// Empty storage if all the coordinates are zeros. This allows us to
330    /// bypass variation processing for the default instance with a simple
331    /// is_empty() check.
332    fn clear_if_all_zeroes(&mut self) {
333        match self {
334            Self::None => {}
335            Self::Inline(coords, len) => {
336                if coords[..*len as usize]
337                    .iter()
338                    .all(|&c| c == NormalizedCoord::ZERO)
339                {
340                    *len = 0;
341                }
342            }
343            Self::Heap(heap) => {
344                if heap.iter().all(|&c| c == NormalizedCoord::ZERO) {
345                    heap.clear();
346                }
347            }
348        }
349    }
350
351    fn resize(&mut self, new_len: usize) {
352        match self {
353            Self::None => {
354                if new_len > MAX_INLINE_COORDS {
355                    let mut heap = Vec::with_capacity(new_len);
356                    heap.resize(new_len, NormalizedCoord::ZERO);
357                    *self = Self::Heap(heap);
358                } else {
359                    *self = Self::Inline([NormalizedCoord::ZERO; MAX_INLINE_COORDS], new_len as u8);
360                }
361            }
362            Self::Inline(_, len) => {
363                if new_len > MAX_INLINE_COORDS {
364                    let mut heap = Vec::with_capacity(new_len);
365                    heap.resize(new_len, NormalizedCoord::ZERO);
366                    *self = Self::Heap(heap);
367                } else {
368                    *len = new_len as u8;
369                }
370            }
371            Self::Heap(heap) => {
372                heap.resize(new_len, NormalizedCoord::ZERO);
373            }
374        }
375    }
376
377    fn as_slice(&self) -> &[NormalizedCoord] {
378        match self {
379            Self::None => &[],
380            Self::Inline(coords, len) => &coords[..*len as usize],
381            Self::Heap(heap) => heap.as_slice(),
382        }
383    }
384
385    fn as_mut_slice(&mut self) -> &mut [NormalizedCoord] {
386        match self {
387            Self::None => &mut [],
388            Self::Inline(coords, len) => &mut coords[..*len as usize],
389            Self::Heap(heap) => heap.as_mut_slice(),
390        }
391    }
392}
393
394/// Feature variation selections for the layout tables.
395#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
396pub struct FontFeatureVariations {
397    gsub: Option<u32>,
398    gpos: Option<u32>,
399}
400
401impl FontFeatureVariations {
402    /// Returns the selected feature variation index for the GSUB table, if any.
403    pub fn gsub(&self) -> Option<u32> {
404        self.gsub
405    }
406
407    /// Returns the selected feature variation index for the GPOS table, if any.
408    pub fn gpos(&self) -> Option<u32> {
409        self.gpos
410    }
411}
412
413/// Lazy atomic storage for feature variation selections.
414///
415/// We don't want to load the GSUB and GPOS tables unless explicitly requested.
416struct FeatureVarsStorage {
417    status: AtomicU32,
418    gsub: AtomicU32,
419    gpos: AtomicU32,
420}
421
422impl Default for FeatureVarsStorage {
423    fn default() -> Self {
424        Self::new()
425    }
426}
427
428impl FeatureVarsStorage {
429    /// We haven't checked yet.
430    const UNCHECKED: u32 = 0;
431    /// We have a selected feature variation.
432    const PRESENT: u32 = 1;
433    /// We don't have a selected feature variation.
434    const ABSENT: u32 = 2;
435    /// Both GSUB and GPOS don't have a selected feature variation.
436    const BOTH_ABSENT: u32 = Self::ABSENT | (Self::ABSENT << Self::GPOS_SHIFT);
437    /// GPOS status is packed in the high 16 bits. GSUB status is packed in the
438    /// low 16 bits.
439    const GPOS_SHIFT: u32 = 16;
440
441    fn new() -> Self {
442        Self {
443            status: AtomicU32::new(Self::UNCHECKED),
444            gsub: AtomicU32::new(0),
445            gpos: AtomicU32::new(0),
446        }
447    }
448
449    fn load(&self, font: &Font, coords: &[NormalizedCoord]) -> FontFeatureVariations {
450        let mut status = self.status.load(atomic::Ordering::Acquire);
451        if status == Self::UNCHECKED {
452            let tables = font.tables();
453            let feature_var_tables = [
454                tables
455                    .gsub()
456                    .ok()
457                    .and_then(|gsub| gsub.feature_variations().transpose().ok().flatten()),
458                tables
459                    .gpos()
460                    .ok()
461                    .and_then(|gpos| gpos.feature_variations().transpose().ok().flatten()),
462            ];
463            for (i, (table, state)) in feature_var_tables
464                .iter()
465                .zip([&self.gsub, &self.gpos])
466                .enumerate()
467            {
468                let mut table_status = Self::ABSENT;
469                if let Some(table) = table {
470                    if let Some(index) = feature_variation_index(table, coords) {
471                        state.store(index, atomic::Ordering::Release);
472                        table_status = Self::PRESENT;
473                    }
474                }
475                status |= table_status << (i * Self::GPOS_SHIFT as usize);
476            }
477            self.status.store(status, atomic::Ordering::Release);
478        }
479        if status != Self::BOTH_ABSENT {
480            let gsub_status = status & 0xFFFF;
481            let gpos_status = (status >> Self::GPOS_SHIFT) & 0xFFFF;
482            FontFeatureVariations {
483                gsub: (gsub_status == Self::PRESENT)
484                    .then(|| self.gsub.load(atomic::Ordering::Acquire)),
485                gpos: (gpos_status == Self::PRESENT)
486                    .then(|| self.gpos.load(atomic::Ordering::Acquire)),
487            }
488        } else {
489            FontFeatureVariations::default()
490        }
491    }
492}
493
494pub(crate) fn feature_variation_index(
495    feature_vars: &FeatureVariations,
496    coords: &[NormalizedCoord],
497) -> Option<u32> {
498    for (index, rec) in feature_vars.feature_variation_records().iter().enumerate() {
499        // If the ConditionSet offset is 0, this is treated as the
500        // universal condition: all contexts are matched.
501        if rec.condition_set_offset().is_null() {
502            return Some(index as u32);
503        }
504        let Some(Ok(condition_set)) = rec.condition_set(feature_vars.offset_data()) else {
505            continue;
506        };
507        // Otherwise, all conditions must be satisfied.
508        if condition_set
509            .conditions()
510            .iter()
511            // .. except we ignore errors
512            .filter_map(Result::ok)
513            .all(|cond| match cond {
514                Condition::Format1AxisRange(format1) => {
515                    let coord = coords
516                        .get(format1.axis_index() as usize)
517                        .copied()
518                        .unwrap_or_default();
519                    coord >= format1.filter_range_min_value()
520                        && coord <= format1.filter_range_max_value()
521                }
522                _ => false,
523            })
524        {
525            return Some(index as u32);
526        }
527    }
528    None
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534    use core::sync::atomic::Ordering;
535
536    #[test]
537    fn named_instances() {
538        let font = Font::new(font_test_data::CANTARELL_VF_TRIMMED, 0).unwrap();
539        let cases = [
540            // (named instance index, expected weight value)
541            (0, 100.0),
542            (1, 300.0),
543            (2, 400.0),
544            (3, 700.0),
545            (4, 800.0),
546        ];
547        for (index, weight) in cases {
548            let named_instance = FontInstance::builder(&font).named_instance(index).build();
549            let var_instance = FontInstance::builder(&font)
550                .variations([("wght", weight)])
551                .build();
552            assert_eq!(
553                named_instance.normalized_coords(),
554                var_instance.normalized_coords(),
555                "index={index}"
556            );
557        }
558        // Out of bounds index should give us the default instance.
559        let invalid_instance = FontInstance::builder(&font).named_instance(5).build();
560        assert!(
561            invalid_instance.normalized_coords().is_empty(),
562            "out of bounds index should give default instance"
563        );
564    }
565
566    #[test]
567    fn named_instance_with_overrides_override_named_value() {
568        let font = Font::new(font_test_data::MATERIAL_SYMBOLS_SUBSET, 0).unwrap();
569        let actual = FontInstance::builder(&font)
570            .named_instance_with_overrides(3, [("FILL", 1.0)])
571            .build();
572        let expected = FontInstance::builder(&font)
573            .variations([
574                ("FILL", 1.0),
575                ("GRAD", 0.0),
576                ("opsz", 24.0),
577                ("wght", 400.0),
578            ])
579            .build();
580        assert_eq!(actual.normalized_coords(), expected.normalized_coords());
581    }
582
583    #[test]
584    fn named_instance_with_overrides_invalid_index_uses_overrides_only() {
585        let font = Font::new(font_test_data::MATERIAL_SYMBOLS_SUBSET, 0).unwrap();
586        let actual = FontInstance::builder(&font)
587            .named_instance_with_overrides(999, [("FILL", 1.0), ("ZZZZ", 123.0)])
588            .build();
589        let expected = FontInstance::builder(&font)
590            .variations([("FILL", 1.0), ("ZZZZ", 123.0)])
591            .build();
592        assert_eq!(actual.normalized_coords(), expected.normalized_coords());
593        assert_eq!(actual.normalized_coords().len(), 4);
594    }
595
596    #[test]
597    fn named_instance_with_overrides_overwrites_previous_settings() {
598        let font = Font::new(font_test_data::MATERIAL_SYMBOLS_SUBSET, 0).unwrap();
599        let actual = FontInstance::builder(&font)
600            .variations([("FILL", 0.0), ("wght", 100.0)])
601            .named_instance_with_overrides(5, [("GRAD", -25.0)])
602            .build();
603        let expected = FontInstance::builder(&font)
604            .variations([
605                ("FILL", 0.0),
606                ("GRAD", -25.0),
607                ("opsz", 24.0),
608                ("wght", 600.0),
609            ])
610            .build();
611        assert_eq!(actual.normalized_coords(), expected.normalized_coords());
612    }
613
614    #[test]
615    fn feature_variations() {
616        let font = Font::new(font_test_data::MATERIAL_SYMBOLS_SUBSET, 0).unwrap();
617        let cases = [
618            // (fill, [GSUB feature variation index, GPOS feature variation index])
619            (0.0, [None, None]),
620            (0.5, [None, None]),
621            (0.98, [None, None]),
622            (0.99, [Some(0), None]),
623            (1.0, [Some(0), None]),
624        ];
625        for (fill, [gsub, gpos]) in cases {
626            let instance = FontInstance::builder(&font)
627                .variations([("FILL", fill)])
628                .build();
629            let feature_vars = instance.feature_variations();
630            let actual = [feature_vars.gsub(), feature_vars.gpos()];
631            assert_eq!(actual, [gsub, gpos], "fill={fill}");
632        }
633    }
634
635    #[test]
636    fn feature_variation_cache_marks_both_absent() {
637        let font = Font::new(font_test_data::MATERIAL_SYMBOLS_SUBSET, 0).unwrap();
638        let instance = FontInstance::builder(&font)
639            .variations([("FILL", 0.5)])
640            .build();
641        assert_eq!(instance.feature_vars.status.load(Ordering::Acquire), 0);
642        assert_eq!(
643            instance.feature_variations(),
644            FontFeatureVariations::default()
645        );
646        assert_eq!(
647            instance.feature_vars.status.load(Ordering::Acquire),
648            FeatureVarsStorage::BOTH_ABSENT
649        );
650    }
651
652    #[test]
653    fn feature_variation_cache_is_thread_safe_and_stable() {
654        let font = Font::new(font_test_data::MATERIAL_SYMBOLS_SUBSET, 0).unwrap();
655        let instance = FontInstance::builder(&font)
656            .variations([("FILL", 1.0)])
657            .build();
658        std::thread::scope(|scope| {
659            for _ in 0..8 {
660                scope.spawn(|| {
661                    for _ in 0..64 {
662                        let vars = instance.feature_variations();
663                        assert_eq!(
664                            vars,
665                            FontFeatureVariations {
666                                gsub: Some(0),
667                                gpos: None
668                            }
669                        );
670                    }
671                });
672            }
673        });
674        let status = instance.feature_vars.status.load(Ordering::Acquire);
675        assert_eq!(status & 0xFFFF, FeatureVarsStorage::PRESENT);
676        assert_eq!(
677            (status >> FeatureVarsStorage::GPOS_SHIFT) & 0xFFFF,
678            FeatureVarsStorage::ABSENT
679        );
680        assert_eq!(instance.feature_vars.gsub.load(Ordering::Acquire), 0);
681    }
682
683    #[test]
684    fn variations_last_value_wins_and_unknown_axis_ignored() {
685        let font = Font::new(font_test_data::CANTARELL_VF_TRIMMED, 0).unwrap();
686        let expected = FontInstance::builder(&font)
687            .variations([("wght", 700.0)])
688            .build();
689        let repeated_axis = FontInstance::builder(&font)
690            .variations([("wght", 100.0), ("wght", 700.0)])
691            .build();
692        assert_eq!(
693            repeated_axis.normalized_coords(),
694            expected.normalized_coords()
695        );
696        let unknown_axis = FontInstance::builder(&font)
697            .variations([("wght", 700.0), ("ZZZZ", 123.0)])
698            .build();
699        assert_eq!(
700            unknown_axis.normalized_coords(),
701            expected.normalized_coords()
702        );
703    }
704
705    #[test]
706    fn later_variation_call_overwrites_previous() {
707        let font = Font::new(font_test_data::CANTARELL_VF_TRIMMED, 0).unwrap();
708        let overwritten = FontInstance::builder(&font)
709            .variations([("wght", 700.0)])
710            .variations([("wght", 100.0)])
711            .build();
712        let expected = FontInstance::builder(&font)
713            .variations([("wght", 100.0)])
714            .build();
715        assert_eq!(
716            overwritten.normalized_coords(),
717            expected.normalized_coords()
718        );
719    }
720
721    #[test]
722    fn normalized_coords_empty_resets_to_default_instance() {
723        let font = Font::new(font_test_data::MATERIAL_SYMBOLS_SUBSET, 0).unwrap();
724        let instance = FontInstance::builder(&font).normalized_coords([]).build();
725        assert!(instance.normalized_coords().is_empty());
726    }
727
728    #[test]
729    fn normalized_coords_truncates_and_pads() {
730        let font = Font::new(font_test_data::MATERIAL_SYMBOLS_SUBSET, 0).unwrap();
731        let axis_count = font.tables().fvar().unwrap().axis_count() as usize;
732        let values = [0.25, -0.5, 1.0, 0.75, -0.25].map(NormalizedCoord::from_f32);
733        let instance = FontInstance::builder(&font)
734            .normalized_coords(values)
735            .build();
736        let coords = instance.normalized_coords();
737        assert_eq!(coords.len(), axis_count);
738        let copied = values.len().min(axis_count);
739        assert_eq!(&coords[..copied], &values[..copied]);
740        assert!(coords[copied..]
741            .iter()
742            .all(|&coord| coord == NormalizedCoord::ZERO));
743    }
744}