diff --git a/fontbe/src/glyphs.rs b/fontbe/src/glyphs.rs index 1033cbc5e..80f4d4943 100644 --- a/fontbe/src/glyphs.rs +++ b/fontbe/src/glyphs.rs @@ -6,7 +6,7 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use fontdrasil::{ - coords::{Location, NormalizedCoord, NormalizedLocation}, + coords::NormalizedLocation, orchestration::{Access, AccessBuilder, Work}, types::GlyphName, }; @@ -247,7 +247,7 @@ fn compute_deltas( glyph_name: &GlyphName, var_model: &VariationModel, should_iup: bool, - point_seqs: &HashMap, Vec>, + point_seqs: &HashMap>, coords: &Vec, contour_ends: &Vec, ) -> Result { diff --git a/fontdrasil/src/coords.rs b/fontdrasil/src/coords.rs index ce5204abf..f283e86fd 100644 --- a/fontdrasil/src/coords.rs +++ b/fontdrasil/src/coords.rs @@ -5,53 +5,145 @@ use std::{ collections::{BTreeMap, HashMap}, fmt::{Debug, Write}, + marker::PhantomData, ops::Sub, }; use ordered_float::OrderedFloat; -use serde::{Deserialize, Serialize}; +use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize}; use write_fonts::types::{F2Dot14, Fixed, Tag}; use crate::{ - piecewise_linear_map::PiecewiseLinearMap, - serde::{CoordConverterSerdeRepr, LocationSerdeRepr}, - types::Axis, + piecewise_linear_map::PiecewiseLinearMap, serde::CoordConverterSerdeRepr, types::Axis, }; -/// A coordinate in some arbitrary space the designer dreamed up. +/// A trait for converting coordinates between coordinate spaces. /// -/// In .designspace, an xvalue. . -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DesignCoord(OrderedFloat); +/// This trait is intended to be implemented on types that represent the +/// coordinate spaces themselves. +/// +/// You don't ever need to use this directly; it is used to implement the +/// [`Coord::convert`] and [`Location::convert`] methods. +pub trait ConvertSpace: Sized { + /// Convert a coord from our space to the target space. + fn convert_coord(coord: Coord, converter: &CoordConverter) -> Coord; +} -/// A coordinate the end user sees, e.g. what 'fvar' uses, Weight 400. +/// The coordinate space used by the type designer/editing software. +/// +/// This space has arbitrary bounds defined on a per-project basis. For instance, +/// a font might internally represent the 'weight' axis as a value from 0-200. +/// +/// In [.designspace file][dspace], this is an 'xvalue'. /// -/// In .designspace, a uservalue. . -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct UserCoord(OrderedFloat); +/// [dspace]: https://fonttools.readthedocs.io/en/latest/designspaceLib/xml.html#dimension-element +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DesignSpace; -/// A coordinate used within the font, not seen by any user. +/// A coordinate space that may be visible to the end user. /// -/// Always in [-1, 1]. +/// For instance a weight value in CSS is expressed in user coordinates. /// -/// Not typically used directly in sources. -#[derive( - Serialize, Deserialize, Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, -)] -pub struct NormalizedCoord(OrderedFloat); +/// In a [.designspace file][dspace], this is a 'uservalue'. +/// +/// [dspace]: https://fonttools.readthedocs.io/en/latest/designspaceLib/xml.html#dimension-element +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct UserSpace; + +/// A space containing only values in the range `-1..=1`. +/// +/// This is used internally in the font, and is never visible to the user. +/// The default value is always at `0`. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NormalizedSpace; + +/// A coordinate in some coordinate space. +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Coord { + coord: OrderedFloat, + // we want to be covariant but also Send + Sync. See + // + space: PhantomData Space>, +} + +/// A coordinate in design space. +pub type DesignCoord = Coord; +/// A coordinate in user space +pub type UserCoord = Coord; +/// A coordinate in normalized space +pub type NormalizedCoord = Coord; + +impl Coord { + /// Create a new coordinate. + /// + /// Note that we do *not* impl From because we want conversion to be explicit. + pub fn new(value: impl Into>) -> Self { + //TODO: Code review: this constructor can create invalid values, + //particularly in the case of NormalizedCoords. We could fix this + //a number of ways, for instance by clamping the space or else by making + //it fallible. Do we care? What solution would we prefer? + Coord { + coord: value.into(), + space: PhantomData, + } + } + + pub fn into_inner(self) -> OrderedFloat { + self.coord + } + + pub fn to_f32(&self) -> f32 { + self.coord.into_inner() + } + + /// Convert this coordinate into the target space. + pub fn convert(&self, converter: &CoordConverter) -> Coord + where + Space: ConvertSpace, + { + Space::convert_coord(*self, converter) + } +} /// A set of per-axis coordinates that define a specific location in a coordinate system. /// -/// E.g. a user location is a `Location`. Hashable so it can do things like be +/// E.g. a user location is a `Location`. Hashable so it can do things like be /// the key for a map of sources by location. -#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[serde(from = "LocationSerdeRepr", into = "LocationSerdeRepr")] -// we need this trait bound here for our serde impl to work -pub struct Location(pub(crate) BTreeMap); +#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Location(BTreeMap>); + +/// A location in [`DesignSpace`]. +pub type DesignLocation = Location; +/// A location in [`UserSpace`]. +pub type UserLocation = Location; +/// A location in [`NormalizedSpace`]. +pub type NormalizedLocation = Location; + +// a little helper to generate methods on coord/location for specific conversions +macro_rules! convert_convenience_methods { + ($space:ident, $fn_name:ident) => { + impl Coord + where + Space: ConvertSpace<$space>, + { + pub fn $fn_name(&self, converter: &CoordConverter) -> Coord<$space> { + self.convert(converter) + } + } -pub type DesignLocation = Location; -pub type UserLocation = Location; -pub type NormalizedLocation = Location; + impl Location + where + Space: ConvertSpace<$space>, + { + pub fn $fn_name(&self, axes: &HashMap) -> Location<$space> { + self.convert(axes) + } + } + }; +} +convert_convenience_methods!(NormalizedSpace, to_normalized); +convert_convenience_methods!(UserSpace, to_user); +convert_convenience_methods!(DesignSpace, to_design); /// Converts between Design, User, and Normalized coordinates. /// @@ -71,7 +163,7 @@ impl CoordConverter { /// Initialize a converter from the User:Design examples source files typically provide. pub fn new(mut mappings: Vec<(UserCoord, DesignCoord)>, default_idx: usize) -> CoordConverter { if mappings.is_empty() { - mappings.push((UserCoord(0.0.into()), DesignCoord(0.0.into()))); + mappings.push((UserCoord::new(0.0), DesignCoord::new(0.0))); } let user_to_design = PiecewiseLinearMap::new( mappings @@ -155,7 +247,7 @@ impl<'a> Iterator for CoordConverterIter<'a> { let user = UserCoord::new(self.converter.user_to_design.from[self.idx]); let design = DesignCoord::new(self.converter.user_to_design.to[self.idx]); - let normalized = user.to_normalized(self.converter); + let normalized = user.convert(self.converter); let result = (user, design, normalized); self.idx += 1; @@ -163,105 +255,44 @@ impl<'a> Iterator for CoordConverterIter<'a> { } } -impl DesignCoord { - /// We do *not* provide From because we want conversion to be explicit - pub fn new(value: impl Into>) -> DesignCoord { - DesignCoord(value.into()) - } - - pub fn to_user(&self, converter: &CoordConverter) -> UserCoord { - UserCoord::new(converter.design_to_user.map(self.0)) - } - - pub fn to_normalized(&self, converter: &CoordConverter) -> NormalizedCoord { - NormalizedCoord::new(converter.design_to_normalized.map(self.0)) - } - - pub fn into_inner(self) -> OrderedFloat { - self.0 - } -} - -impl UserCoord { - /// We do *not* provide From because we want conversion to be explicit - pub fn new(value: impl Into>) -> UserCoord { - UserCoord(value.into()) - } - - pub fn to_design(&self, converter: &CoordConverter) -> DesignCoord { - DesignCoord::new(converter.user_to_design.map(self.0)) - } - - pub fn to_normalized(&self, converter: &CoordConverter) -> NormalizedCoord { - self.to_design(converter).to_normalized(converter) - } - - pub fn into_inner(self) -> OrderedFloat { - self.0 - } -} - impl From for Fixed { fn from(value: UserCoord) -> Self { - Fixed::from_f64(value.0.into_inner() as f64) - } -} - -impl NormalizedCoord { - /// We do *not* provide From because we want conversion to be explicit - pub fn new(value: impl Into>) -> NormalizedCoord { - NormalizedCoord(value.into()) - } - - pub fn to_design(&self, converter: &CoordConverter) -> DesignCoord { - DesignCoord::new(converter.normalized_to_design.map(self.0)) - } - - pub fn to_user(&self, converter: &CoordConverter) -> UserCoord { - self.to_design(converter).to_user(converter) - } - - pub fn into_inner(self) -> OrderedFloat { - self.0 - } - - pub fn to_f32(&self) -> f32 { - self.0.into_inner() + Fixed::from_f64(value.to_f32() as f64) } } impl From for F2Dot14 { fn from(value: NormalizedCoord) -> Self { - F2Dot14::from_f32(value.0.into_inner()) + F2Dot14::from_f32(value.to_f32()) } } -impl Sub for NormalizedCoord { - type Output = NormalizedCoord; +impl Sub> for Coord { + type Output = Coord; - fn sub(self, rhs: NormalizedCoord) -> Self::Output { - NormalizedCoord::new(self.0 - rhs.0) + fn sub(self, rhs: Coord) -> Self::Output { + Coord::new(self.to_f32() - rhs.to_f32()) } } -impl FromIterator<(Tag, T)> for Location { - fn from_iter>(iter: I) -> Self { - Location(BTreeMap::from_iter(iter)) +impl FromIterator<(Tag, Coord)> for Location { + fn from_iter)>>(iter: I) -> Self { + Location(iter.into_iter().collect()) } } -impl From> for Location { - fn from(value: Vec<(Tag, T)>) -> Self { - Location::::from_iter(value) +impl From)>> for Location { + fn from(value: Vec<(Tag, Coord)>) -> Self { + value.into_iter().collect() } } -impl Location { - pub fn new() -> Location { - Location(BTreeMap::new()) +impl Location { + pub fn new() -> Location { + Location(Default::default()) } - pub fn insert(&mut self, tag: Tag, pos: T) -> &mut Location { + pub fn insert(&mut self, tag: Tag, pos: Coord) -> &mut Location { self.0.insert(tag, pos); self } @@ -270,7 +301,7 @@ impl Location { self.0.remove(&tag); } - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator)> { self.0.iter() } @@ -282,76 +313,125 @@ impl Location { self.0.contains_key(&tag) } - pub fn get(&self, tag: Tag) -> Option { + pub fn get(&self, tag: Tag) -> Option> { self.0.get(&tag).copied() } - pub fn retain(&mut self, pred: impl Fn(&Tag, &mut T) -> bool) { + pub fn retain(&mut self, pred: impl Fn(&Tag, &mut Coord) -> bool) { self.0.retain(pred); } + + pub fn convert(&self, axes: &HashMap) -> Location + where + Space: ConvertSpace, + { + self.0 + .iter() + .map(|(tag, coord)| (*tag, coord.convert(&axes.get(tag).unwrap().converter))) + .collect() + } } -impl UserLocation { - pub fn to_normalized(&self, axes: &HashMap) -> NormalizedLocation { - Location::( - self.0 - .iter() - .map(|(tag, dc)| (*tag, dc.to_normalized(&axes.get(tag).unwrap().converter))) - .collect(), - ) +// methods we only want available on NormalizedSpace +impl Location { + pub fn has_non_zero(&self, tag: Tag) -> bool { + self.get(tag).unwrap_or_default().to_f32() != 0.0 } - pub fn to_design(&self, axes: &HashMap) -> DesignLocation { - Location::( - self.0 - .iter() - .map(|(tag, coord)| (*tag, coord.to_design(&axes.get(tag).unwrap().converter))) - .collect(), - ) + pub fn has_any_non_zero(&self) -> bool { + self.0.values().any(|v| v.to_f32() != 0.0) + } + + /// Returns true if all normalized coordinates are zero + pub fn is_default(&self) -> bool { + !self.has_any_non_zero() } } -impl DesignLocation { - pub fn to_normalized(&self, axes: &HashMap) -> NormalizedLocation { - Location::( - self.0 - .iter() - .map(|(tag, dc)| (*tag, dc.to_normalized(&axes.get(tag).unwrap().converter))) - .collect(), - ) +impl ConvertSpace for DesignSpace { + fn convert_coord(coord: Coord, converter: &CoordConverter) -> Coord { + Coord::new(converter.design_to_user.map(coord.coord)) } +} - pub fn to_user(&self, axes: &HashMap) -> UserLocation { - Location::( - self.0 - .iter() - .map(|(tag, coord)| (*tag, coord.to_user(&axes.get(tag).unwrap().converter))) - .collect(), - ) +impl ConvertSpace for DesignSpace { + fn convert_coord(coord: Coord, converter: &CoordConverter) -> Coord { + Coord::new(converter.design_to_normalized.map(coord.coord)) } } -impl NormalizedLocation { - pub fn to_user(&self, axes: &HashMap) -> UserLocation { - Location::( - self.0 - .iter() - .map(|(tag, coord)| (*tag, coord.to_user(&axes.get(tag).unwrap().converter))) - .collect(), - ) +impl ConvertSpace for UserSpace { + fn convert_coord(coord: Coord, converter: &CoordConverter) -> Coord { + Coord::new(converter.user_to_design.map(coord.coord)) } +} - pub fn has_non_zero(&self, tag: Tag) -> bool { - self.get(tag).unwrap_or_default().into_inner() != OrderedFloat(0.0) +impl ConvertSpace for UserSpace { + fn convert_coord(coord: Coord, converter: &CoordConverter) -> Coord { + let dspace: DesignCoord = UserSpace::convert_coord(coord, converter); + DesignSpace::convert_coord(dspace, converter) } +} - pub fn has_any_non_zero(&self) -> bool { - self.0.values().any(|v| v.into_inner() != OrderedFloat(0.0)) +impl ConvertSpace for NormalizedSpace { + fn convert_coord(coord: Coord, converter: &CoordConverter) -> Coord { + Coord::new(converter.normalized_to_design.map(coord.coord)) } +} - /// Returns true if all normalized coordinates are zero - pub fn is_default(&self) -> bool { - !self.has_any_non_zero() +impl ConvertSpace for NormalizedSpace { + fn convert_coord(coord: Coord, converter: &CoordConverter) -> Coord { + let dspace: DesignCoord = NormalizedSpace::convert_coord(coord, converter); + DesignSpace::convert_coord(dspace, converter) + } +} + +// we need to manually implement this bc of phantomdata: +// +impl Clone for Coord { + fn clone(&self) -> Self { + Self { + coord: self.coord.clone(), + space: PhantomData, + } + } +} + +impl Copy for Coord {} + +impl Default for Coord { + fn default() -> Self { + Self { + coord: Default::default(), + space: PhantomData, + } + } +} +impl Serialize for Location { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.0.len()))?; + for (key, value) in &self.0 { + seq.serialize_element(&(key, value.to_f32()))?; + } + seq.end() + } +} + +impl<'de, Space> Deserialize<'de> for Location { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Vec::<(Tag, OrderedFloat)>::deserialize(deserializer).map(|vals| { + Location( + vals.into_iter() + .map(|(tag, val)| (tag, Coord::new(val))) + .collect(), + ) + }) } } @@ -402,15 +482,15 @@ mod tests { fn lexend_weight_mapping() -> (Vec<(UserCoord, DesignCoord)>, usize) { ( vec![ - (UserCoord(100.0.into()), DesignCoord(26.0.into())), - (UserCoord(200.0.into()), DesignCoord(39.0.into())), - (UserCoord(300.0.into()), DesignCoord(58.0.into())), - (UserCoord(400.0.into()), DesignCoord(90.0.into())), // [3]; default - (UserCoord(500.0.into()), DesignCoord(108.0.into())), - (UserCoord(600.0.into()), DesignCoord(128.0.into())), - (UserCoord(700.0.into()), DesignCoord(151.0.into())), - (UserCoord(800.0.into()), DesignCoord(169.0.into())), - (UserCoord(900.0.into()), DesignCoord(190.0.into())), + (UserCoord::new(100.0), DesignCoord::new(26.0)), + (UserCoord::new(200.0), DesignCoord::new(39.0)), + (UserCoord::new(300.0), DesignCoord::new(58.0)), + (UserCoord::new(400.0), DesignCoord::new(90.0)), // [3]; default + (UserCoord::new(500.0), DesignCoord::new(108.0)), + (UserCoord::new(600.0), DesignCoord::new(128.0)), + (UserCoord::new(700.0), DesignCoord::new(151.0)), + (UserCoord::new(800.0), DesignCoord::new(169.0)), + (UserCoord::new(900.0), DesignCoord::new(190.0)), ], 3, ) @@ -420,11 +500,11 @@ mod tests { fn bendy_mapping() -> (Vec<(UserCoord, DesignCoord)>, usize) { ( vec![ - (UserCoord(100.0.into()), DesignCoord(0.0.into())), - (UserCoord(200.0.into()), DesignCoord(1.0.into())), - (UserCoord(400.0.into()), DesignCoord(10.0.into())), // [2]; default - (UserCoord(500.0.into()), DesignCoord(19.0.into())), - (UserCoord(900.0.into()), DesignCoord(20.0.into())), + (UserCoord::new(100.0), DesignCoord::new(0.0)), + (UserCoord::new(200.0), DesignCoord::new(1.0)), + (UserCoord::new(400.0), DesignCoord::new(10.0)), // [2]; default + (UserCoord::new(500.0), DesignCoord::new(19.0)), + (UserCoord::new(900.0), DesignCoord::new(20.0)), ], 2, ) @@ -436,19 +516,19 @@ mod tests { let converter = CoordConverter::new(examples, default_idx); assert_eq!( OrderedFloat(-1.0), - DesignCoord(26.0.into()) + DesignCoord::new(26.0) .to_normalized(&converter) .into_inner() ); assert_eq!( OrderedFloat(0.0), - DesignCoord(90.0.into()) + DesignCoord::new(90.0) .to_normalized(&converter) .into_inner() ); assert_eq!( OrderedFloat(1.0), - DesignCoord(190.0.into()) + DesignCoord::new(190.0) .to_normalized(&converter) .into_inner() ); @@ -463,31 +543,27 @@ mod tests { // But design:normalized doesn't care, it's linear from default=>max and default=>min assert_eq!( OrderedFloat(-1.0), - DesignCoord(0.0.into()) - .to_normalized(&converter) - .into_inner() + DesignCoord::new(0.0).to_normalized(&converter).into_inner() ); assert_eq!( OrderedFloat(-0.5), - DesignCoord(5.0.into()) - .to_normalized(&converter) - .into_inner() + DesignCoord::new(5.0).to_normalized(&converter).into_inner() ); assert_eq!( OrderedFloat(0.0), - DesignCoord(10.0.into()) + DesignCoord::new(10.0) .to_normalized(&converter) .into_inner() ); assert_eq!( OrderedFloat(0.5), - DesignCoord(15.0.into()) + DesignCoord::new(15.0) .to_normalized(&converter) .into_inner() ); assert_eq!( OrderedFloat(1.0), - DesignCoord(20.0.into()) + DesignCoord::new(20.0) .to_normalized(&converter) .into_inner() ); @@ -504,58 +580,48 @@ mod tests { // 150 is halfway between 100 and 200 assert_eq!( OrderedFloat(0.0), - UserCoord(100.0.into()).to_design(&converter).into_inner() + UserCoord::new(100.0).to_design(&converter).into_inner() ); assert_eq!( OrderedFloat(0.5), - UserCoord(150.0.into()).to_design(&converter).into_inner() + UserCoord::new(150.0).to_design(&converter).into_inner() ); assert_eq!( OrderedFloat(1.0), - UserCoord(200.0.into()).to_design(&converter).into_inner() + UserCoord::new(200.0).to_design(&converter).into_inner() ); assert_eq!( OrderedFloat(-1.0), - UserCoord(100.0.into()) - .to_normalized(&converter) - .into_inner() + UserCoord::new(100.0).to_normalized(&converter).into_inner() ); assert_eq!( OrderedFloat(-0.95), - UserCoord(150.0.into()) - .to_normalized(&converter) - .into_inner() + UserCoord::new(150.0).to_normalized(&converter).into_inner() ); assert_eq!( OrderedFloat(-0.9), - UserCoord(200.0.into()) - .to_normalized(&converter) - .into_inner() + UserCoord::new(200.0).to_normalized(&converter).into_inner() ); // 200..400 covers a massive slice! // 300 is halway to 400 (breaking news!) assert_eq!( OrderedFloat(5.5), - UserCoord(300.0.into()).to_design(&converter).into_inner() + UserCoord::new(300.0).to_design(&converter).into_inner() ); assert_eq!( OrderedFloat(10.0), - UserCoord(400.0.into()).to_design(&converter).into_inner() + UserCoord::new(400.0).to_design(&converter).into_inner() ); assert_eq!( OrderedFloat(-0.45), - UserCoord(300.0.into()) - .to_normalized(&converter) - .into_inner() + UserCoord::new(300.0).to_normalized(&converter).into_inner() ); assert_eq!( OrderedFloat(0.0), - UserCoord(400.0.into()) - .to_normalized(&converter) - .into_inner() + UserCoord::new(400.0).to_normalized(&converter).into_inner() ); } @@ -564,9 +630,9 @@ mod tests { // min==default==max assert_eq!( CoordConverter::unmapped( - UserCoord(100.0.into()), - UserCoord(100.0.into()), - UserCoord(100.0.into()), + UserCoord::new(100.0), + UserCoord::new(100.0), + UserCoord::new(100.0), ) .default_idx, 0 @@ -574,9 +640,9 @@ mod tests { // min==default, } -#[derive(Serialize, Deserialize, Debug)] -pub(crate) struct LocationSerdeRepr(Vec<(Tag, T)>); - impl From for CoordConverter { fn from(from: CoordConverterSerdeRepr) -> Self { let examples = from @@ -37,15 +33,3 @@ impl From for CoordConverterSerdeRepr { } } } - -impl From> for Location { - fn from(src: LocationSerdeRepr) -> Location { - Location(src.0.into_iter().collect()) - } -} - -impl From> for LocationSerdeRepr { - fn from(src: Location) -> LocationSerdeRepr { - LocationSerdeRepr(src.0.into_iter().collect()) - } -} diff --git a/ufo2fontir/src/source.rs b/ufo2fontir/src/source.rs index cf5f5f9bf..1088efe15 100644 --- a/ufo2fontir/src/source.rs +++ b/ufo2fontir/src/source.rs @@ -479,7 +479,7 @@ fn default_master(designspace: &DesignSpaceDocument) -> Option<(usize, &designsp .map(|a| { let tag = Tag::from_str(&a.tag).unwrap(); let converter = &axes.get(&tag).unwrap().converter; - (tag, UserCoord::new(a.default).to_design(converter)) + (tag, UserCoord::new(a.default).convert(converter)) }) .collect(); designspace @@ -1767,7 +1767,7 @@ mod tests { static_metadata .variation_model .locations() - .map(|loc| (only_coord(loc).to_user(&wght.converter), only_coord(loc))) + .map(|loc| (only_coord(loc).convert(&wght.converter), only_coord(loc))) .collect::>() ); } @@ -1784,7 +1784,7 @@ mod tests { .map(|(loc, ..)| loc) .collect::>() .into_iter() - .map(|loc| (only_coord(&loc).to_user(&wght.converter), only_coord(&loc))) + .map(|loc| (only_coord(&loc).convert(&wght.converter), only_coord(&loc))) .collect::>(); metric_locations.sort();