diff --git a/keyset-drawing/src/imp/key.rs b/keyset-drawing/src/imp/key.rs index 83793af..f82edc8 100644 --- a/keyset-drawing/src/imp/key.rs +++ b/keyset-drawing/src/imp/key.rs @@ -1,6 +1,6 @@ use geom::{ Angle, Circle, Dot, ExtRect, ExtVec, Length, Path, Point, Rect, RoundRect, Size, ToPath, - Vector, DOT_PER_MM, DOT_PER_UNIT, + Vector, DOT_PER_UNIT, }; use profile::Profile; @@ -75,15 +75,15 @@ pub fn homing(key: &key::Key, options: &Options<'_>) -> Option { key::Homing::Scoop => None, key::Homing::Bar => Some( Rect::from_center_and_size( - center + Size::new(0.0, (profile.homing.bar.y_offset * DOT_PER_MM).get()), - profile.homing.bar.size * DOT_PER_MM, + center + Size::new(0.0, profile.homing.bar.y_offset.get()), + profile.homing.bar.size, ) .to_path(), ), key::Homing::Bump => Some( Circle::from_center_and_diameter( - center + Size::new(0.0, (profile.homing.bump.y_offset * DOT_PER_MM).get()), - profile.homing.bump.diameter * DOT_PER_MM, + center + Size::new(0.0, profile.homing.bump.y_offset.get()), + profile.homing.bump.diameter, ) .to_path(), ), @@ -353,12 +353,9 @@ mod tests { assert_is_close!(path.outline.unwrap().width, options.outline_width); let expected = Rect::from_center_and_size( options.profile.top_with_size(Size::splat(1.0)).center(), - options.profile.homing.bar.size * DOT_PER_MM, + options.profile.homing.bar.size, ) - .translate(Vector::new( - 0.0, - (options.profile.homing.bar.y_offset * DOT_PER_MM).get(), - )); + .translate(Vector::new(0.0, options.profile.homing.bar.y_offset.get())); assert_is_close!(bounds, expected); // Bump @@ -378,12 +375,9 @@ mod tests { assert_is_close!(path.outline.unwrap().width, options.outline_width); let expected = Rect::from_center_and_size( options.profile.top_with_size(Size::splat(1.0)).center(), - Size::splat(options.profile.homing.bump.diameter.get()) * DOT_PER_MM, + Size::splat(options.profile.homing.bump.diameter.get()), ) - .translate(Vector::new( - 0.0, - (options.profile.homing.bump.y_offset * DOT_PER_MM).get(), - )); + .translate(Vector::new(0.0, options.profile.homing.bump.y_offset.get())); assert_is_close!(bounds, expected); // Non-homing key diff --git a/keyset-drawing/src/imp/legend.rs b/keyset-drawing/src/imp/legend.rs index fc90f50..4b264f9 100644 --- a/keyset-drawing/src/imp/legend.rs +++ b/keyset-drawing/src/imp/legend.rs @@ -1,5 +1,5 @@ use font::{Font, Glyph}; -use geom::{Dot, Path, Point, Rect, Scale, ToTransform, Vector}; +use geom::{Dot, Path, Point, Rect, ToTransform, Vector}; use log::warn; use profile::Profile; @@ -30,7 +30,7 @@ pub fn draw( let height = profile.text_height.get(legend.size_idx); // Scale to correct height & flip y-axis - let transform = Scale::new(height / font.cap_height().get()) + let transform = (height / font.cap_height()) .to_transform() .then_scale(1.0, -1.0); let text_path = Path::from_slice(&char_paths) * transform; @@ -38,7 +38,7 @@ pub fn draw( // Calculate legend bounds. For x this is based on actual size while for y we use the base line // and text height so each character (especially symbols) are still aligned across keys let bounds = Rect::new( - Point::new(text_path.bounds.min.x, -height), + Point::new(text_path.bounds.min.x, -height.get()), Point::new(text_path.bounds.max.x, 0.0), ); diff --git a/keyset-drawing/src/lib.rs b/keyset-drawing/src/lib.rs index e347389..9a2fbc0 100644 --- a/keyset-drawing/src/lib.rs +++ b/keyset-drawing/src/lib.rs @@ -164,6 +164,7 @@ impl<'a> Options<'a> { #[cfg(test)] mod tests { + use geom::{Mm, DOT_PER_MM}; use isclose::assert_is_close; use profile::Profile; @@ -186,7 +187,10 @@ mod tests { .show_keys(false) .show_margin(true); - assert_is_close!(options.profile.typ.depth(), 1.0); + assert_is_close!( + options.profile.typ.depth(), + Length::::new(1.0) * DOT_PER_MM + ); assert_eq!(options.font.num_glyphs(), 3); // .notdef, A, V assert_is_close!(options.scale, 2.0); } diff --git a/keyset-drawing/src/svg.rs b/keyset-drawing/src/svg.rs index 8675a1f..8570f45 100644 --- a/keyset-drawing/src/svg.rs +++ b/keyset-drawing/src/svg.rs @@ -105,10 +105,10 @@ mod tests { - - - - + + + + "## ) diff --git a/keyset-profile/src/de/mod.rs b/keyset-profile/src/de/mod.rs index ed52e48..489f948 100644 --- a/keyset-profile/src/de/mod.rs +++ b/keyset-profile/src/de/mod.rs @@ -14,6 +14,39 @@ use super::{BarProps, BumpProps, Profile, TopSurface}; pub use error::{Error, Result}; +impl<'de> Deserialize<'de> for Type { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + #[derive(serde::Deserialize)] + #[serde(tag = "type", rename_all = "kebab-case")] + enum RawType { + Cylindrical { + depth: f32, + }, + Spherical { + depth: f32, + }, + #[serde(alias = "chiclet")] + Flat, + } + + RawType::deserialize(deserializer).map(|typ| { + // Convert to Length + match typ { + RawType::Cylindrical { depth } => Self::Cylindrical { + depth: Length::::new(depth) * DOT_PER_MM, + }, + RawType::Spherical { depth } => Self::Spherical { + depth: Length::::new(depth) * DOT_PER_MM, + }, + RawType::Flat => Self::Flat, + } + }) + } +} + impl<'de> Deserialize<'de> for ScoopProps { fn deserialize(deserializer: D) -> std::result::Result where @@ -28,7 +61,7 @@ impl<'de> Deserialize<'de> for ScoopProps { RawScoopProps::deserialize(deserializer).map(|props| { // Convert to Length Self { - depth: Length::::new(props.depth), + depth: Length::::new(props.depth) * DOT_PER_MM, } }) } @@ -50,8 +83,8 @@ impl<'de> Deserialize<'de> for BarProps { RawBarProps::deserialize(deserializer).map(|props| { // Convert to Length Self { - size: Size::::new(props.width, props.height), - y_offset: Length::::new(props.y_offset), + size: Size::::new(props.width, props.height) * DOT_PER_MM, + y_offset: Length::::new(props.y_offset) * DOT_PER_MM, } }) } @@ -72,8 +105,8 @@ impl<'de> Deserialize<'de> for BumpProps { RawBumpProps::deserialize(deserializer).map(|props| { // Convert to Length Self { - diameter: Length::::new(props.diameter), - y_offset: Length::::new(props.y_offset), + diameter: Length::::new(props.diameter) * DOT_PER_MM, + y_offset: Length::::new(props.y_offset) * DOT_PER_MM, } }) } @@ -192,6 +225,7 @@ impl<'de> Deserialize<'de> for Profile { .legend .into_iter() .map(|(i, props)| { + let height = Length::::new(props.size); let Rect { min: props_min, max: props_max, @@ -200,9 +234,9 @@ impl<'de> Deserialize<'de> for Profile { min: raw_min, max: raw_max, } = raw_data.top.rect(); - let offsets = + let offset = SideOffsets::from_vectors_inner(props_min - raw_min, props_max - raw_max); - ((i, props.size), (i, offsets)) + ((i, height), (i, offset)) }) .unzip(); @@ -228,16 +262,16 @@ mod tests { let bar_props: BarProps = toml::from_str("width = 3.85\nheight = 0.4\ny-offset = 5.05").unwrap(); - assert_is_close!(bar_props.size, Size::::new(3.85, 0.4)); - assert_is_close!(bar_props.y_offset, Length::::new(5.05)); + assert_is_close!(bar_props.size, Size::::new(3.85, 0.4) * DOT_PER_MM); + assert_is_close!(bar_props.y_offset, Length::::new(5.05) * DOT_PER_MM); } #[test] fn deserialize_bump_props() { let bar_props: BumpProps = toml::from_str("diameter = 0.4\ny-offset = -0.2").unwrap(); - assert_is_close!(bar_props.diameter, Length::::new(0.4)); - assert_is_close!(bar_props.y_offset, Length::::new(-0.2)); + assert_is_close!(bar_props.diameter, Length::::new(0.4) * DOT_PER_MM); + assert_is_close!(bar_props.y_offset, Length::::new(-0.2) * DOT_PER_MM); } #[test] diff --git a/keyset-profile/src/lib.rs b/keyset-profile/src/lib.rs index 3f73310..894e150 100644 --- a/keyset-profile/src/lib.rs +++ b/keyset-profile/src/lib.rs @@ -8,28 +8,27 @@ use std::collections::HashMap; use std::sync::OnceLock; use geom::{ - Dot, ExtRect, Length, Mm, Point, Rect, RoundRect, SideOffsets, Size, Unit, Vector, DOT_PER_UNIT, + Dot, ExtRect, Inch, Length, Mm, Point, Rect, RoundRect, SideOffsets, Size, Unit, Vector, + DOT_PER_INCH, DOT_PER_MM, DOT_PER_UNIT, }; use interp::interp_array; use key::Homing; use saturate::SaturatingFrom; #[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "kebab-case"))] pub enum Type { - Cylindrical { depth: f32 }, - Spherical { depth: f32 }, + Cylindrical { depth: Length }, + Spherical { depth: Length }, Flat, } impl Type { #[inline] #[must_use] - pub const fn depth(self) -> f32 { + pub const fn depth(self) -> Length { match self { Self::Cylindrical { depth } | Self::Spherical { depth } => depth, - Self::Flat => 0.0, + Self::Flat => Length::new(0.0), } } } @@ -39,26 +38,26 @@ impl Default for Type { fn default() -> Self { Self::Cylindrical { // 1.0mm is approx the depth of OEM profile - depth: 1.0, + depth: Length::::new(1.0) * DOT_PER_MM, } } } #[derive(Debug, Clone, Copy)] pub struct ScoopProps { - pub depth: Length, + pub depth: Length, } #[derive(Debug, Clone, Copy)] pub struct BarProps { - pub size: Size, - pub y_offset: Length, + pub size: Size, + pub y_offset: Length, } #[derive(Debug, Clone, Copy)] pub struct BumpProps { - pub diameter: Length, - pub y_offset: Length, + pub diameter: Length, + pub y_offset: Length, } #[derive(Debug, Clone, Copy)] @@ -92,28 +91,28 @@ impl Default for HomingProps { Self { default: Homing::Bar, scoop: ScoopProps { - depth: Length::new(2.0 * Type::default().depth()), // 2x the regular depth + depth: Type::default().depth() * 2.0, // 2x the regular depth }, bar: BarProps { - size: Size::new(3.81, 0.51), // = 0.15in, 0.02in - y_offset: Length::new(6.35), // = 0.25in + size: Size::::new(0.15, 0.02) * DOT_PER_INCH, + y_offset: Length::::new(0.25) * DOT_PER_INCH, }, bump: BumpProps { - diameter: Length::new(0.51), // = 0.02in - y_offset: Length::new(0.0), + diameter: Length::::new(0.02) * DOT_PER_INCH, + y_offset: Length::::new(0.0) * DOT_PER_INCH, }, } } } #[derive(Debug, Clone, Copy)] -pub struct TextHeight([f32; Self::NUM_HEIGHTS]); +pub struct TextHeight([Length; Self::NUM_HEIGHTS]); impl TextHeight { const NUM_HEIGHTS: usize = 10; #[must_use] - pub fn new(heights: &HashMap) -> Self { + pub fn new(heights: &HashMap>) -> Self { if heights.is_empty() { Self::default() } else { @@ -123,7 +122,7 @@ impl TextHeight { // use 0.0 font size for 0 let mut vec = Vec::with_capacity(heights.len().saturating_add(1)); vec.push((&0, &0.0)); - vec.extend(heights.iter()); + vec.extend(heights.iter().map(|(i, h)| (i, &h.0))); vec.sort_unstable_by_key(|&(i, _h)| i); vec.into_iter() .map(|(&i, &h)| (f32::saturating_from(i), h)) @@ -131,13 +130,13 @@ impl TextHeight { }; let all_indices = array::from_fn(f32::saturating_from); - Self(interp_array(&indices, &heights, &all_indices)) + Self(interp_array(&indices, &heights, &all_indices).map(Length::::new)) } } #[inline] #[must_use] - pub fn get(&self, size_index: usize) -> f32 { + pub fn get(&self, size_index: usize) -> Length { *self .0 .get(size_index) @@ -151,7 +150,7 @@ impl Default for TextHeight { // From: https://github.com/ijprest/keyboard-layout-editor/blob/d2945e5/kb.css#L113 Self( array::from_fn(|i| 6.0 + 2.0 * f32::saturating_from(i)) - .map(|sz| sz * (DOT_PER_UNIT.0 / 72.0)), + .map(|sz| Length::::new(sz / 72.0) * DOT_PER_UNIT), ) } } @@ -361,30 +360,58 @@ mod tests { #[test] fn test_profile_type_depth() { - assert_is_close!(Type::Cylindrical { depth: 1.0 }.depth(), 1.0); - assert_is_close!(Type::Spherical { depth: 0.5 }.depth(), 0.5); - assert_is_close!(Type::Flat.depth(), 0.0); + assert_is_close!( + Type::Cylindrical { + depth: Length::new(1.0) + } + .depth(), + Length::new(1.0) + ); + assert_is_close!( + Type::Spherical { + depth: Length::new(0.5) + } + .depth(), + Length::new(0.5) + ); + assert_is_close!(Type::Flat.depth(), Length::new(0.0)); } #[test] fn test_profile_type_default() { - assert_matches!(Type::default(), Type::Cylindrical { depth } if depth.is_close(1.0)); + assert_matches!(Type::default(), Type::Cylindrical { depth } if depth.is_close(Length::::new(1.0) * DOT_PER_MM)); } #[test] fn test_homing_props_default() { assert_matches!(HomingProps::default().default, Homing::Bar); - assert_is_close!(HomingProps::default().scoop.depth, Length::new(2.0)); - assert_is_close!(HomingProps::default().bar.size, Size::new(3.81, 0.51)); - assert_is_close!(HomingProps::default().bar.y_offset, Length::new(6.35)); - assert_is_close!(HomingProps::default().bump.diameter, Length::new(0.51)); - assert_is_close!(HomingProps::default().bump.y_offset, Length::new(0.0)); + assert_is_close!( + HomingProps::default().scoop.depth, + Length::::new(2.0) * DOT_PER_MM + ); + assert_is_close!( + HomingProps::default().bar.size, + Size::::new(3.81, 0.508) * DOT_PER_MM + ); + assert_is_close!( + HomingProps::default().bar.y_offset, + Length::::new(6.35) * DOT_PER_MM + ); + assert_is_close!( + HomingProps::default().bump.diameter, + Length::::new(0.508) * DOT_PER_MM + ); + assert_is_close!( + HomingProps::default().bump.y_offset, + Length::::new(0.0) * DOT_PER_MM + ); } #[test] fn test_text_height_new() { - let expected: [_; 10] = - array::from_fn(|i| (6.0 + 2.0 * f32::saturating_from(i)) / 72.0 * DOT_PER_UNIT.get()); + let expected: [_; 10] = array::from_fn(|i| { + Length::new(6.0 + 2.0 * f32::saturating_from(i)) / 72.0 * DOT_PER_UNIT + }); let result = TextHeight::new(&HashMap::new()).0; assert_eq!(expected.len(), result.len()); @@ -394,13 +421,14 @@ mod tests { let expected = [ 0.0, 60.0, 120.0, 180.0, 190.0, 210.0, 230.0, 280.0, 330.0, 380.0, - ]; + ] + .map(Length::::new); let result = TextHeight::new(&HashMap::from([ - (1, 60.0), - (3, 180.0), - (4, 190.0), - (6, 230.0), - (9, 380.0), + (1, Length::new(60.0)), + (3, Length::new(180.0)), + (4, Length::new(190.0)), + (6, Length::new(230.0)), + (9, Length::new(380.0)), ])) .0; @@ -414,14 +442,14 @@ mod tests { #[test] fn test_text_height_get() { let heights = TextHeight::new(&HashMap::from([ - (1, 3.0), - (3, 9.0), - (4, 9.5), - (6, 11.5), - (9, 19.0), + (1, Length::new(3.0)), + (3, Length::new(9.0)), + (4, Length::new(9.5)), + (6, Length::new(11.5)), + (9, Length::new(19.0)), ])); - assert_is_close!(heights.get(5), 10.5); - assert_is_close!(heights.get(23), 19.0); + assert_is_close!(heights.get(5), Length::new(10.5)); + assert_is_close!(heights.get(23), Length::new(19.0)); } #[test] @@ -431,7 +459,7 @@ mod tests { for (i, h) in heights.0.into_iter().enumerate() { assert_is_close!( h, - (6.0 + 2.0 * f32::saturating_from(i)) / 72.0 * DOT_PER_UNIT.get() + Length::new(6.0 + 2.0 * f32::saturating_from(i)) / 72.0 * DOT_PER_UNIT ); } } @@ -607,7 +635,9 @@ mod tests { let profile = Profile::from_toml(PROFILE_TOML).unwrap(); - assert!(matches!(profile.typ, Type::Cylindrical { depth } if depth.is_close(0.5))); + assert!( + matches!(profile.typ, Type::Cylindrical { depth } if depth.is_close(Length::::new(0.5) * DOT_PER_MM)) + ); assert_is_close!(profile.bottom.size, Size::splat(18.29) * DOT_PER_MM); assert_is_close!(profile.bottom.radius, Length::new(0.38) * DOT_PER_MM); @@ -620,7 +650,7 @@ mod tests { let expected = [ 0.0, 0.76, 1.52, 2.28, 3.18, 4.84, 6.5, 8.16, 9.82, 11.48, 13.14, ] - .map(|e| e * DOT_PER_MM.0); + .map(|e| Length::::new(e) * DOT_PER_MM); for (e, r) in expected.iter().zip(profile.text_height.0.iter()) { assert_is_close!(e, r); } @@ -644,11 +674,26 @@ mod tests { } assert_matches!(profile.homing.default, Homing::Scoop); - assert_is_close!(profile.homing.scoop.depth, Length::::new(1.5)); - assert_is_close!(profile.homing.bar.size, Size::::new(3.85, 0.4)); - assert_is_close!(profile.homing.bar.y_offset, Length::::new(5.05)); - assert_is_close!(profile.homing.bump.diameter, Length::::new(0.4)); - assert_is_close!(profile.homing.bump.y_offset, Length::::new(-0.2)); + assert_is_close!( + profile.homing.scoop.depth, + Length::::new(1.5) * DOT_PER_MM + ); + assert_is_close!( + profile.homing.bar.size, + Size::::new(3.85, 0.4) * DOT_PER_MM + ); + assert_is_close!( + profile.homing.bar.y_offset, + Length::::new(5.05) * DOT_PER_MM + ); + assert_is_close!( + profile.homing.bump.diameter, + Length::::new(0.4) * DOT_PER_MM + ); + assert_is_close!( + profile.homing.bump.y_offset, + Length::::new(-0.2) * DOT_PER_MM + ); } #[cfg(feature = "toml")] @@ -737,7 +782,7 @@ mod tests { let profile = Profile::from_json(PROFILE_JSON).unwrap(); - assert_matches!(profile.typ, Type::Cylindrical { depth } if depth.is_close(0.5)); + assert_matches!(profile.typ, Type::Cylindrical { depth } if depth.is_close(Length::::new(0.5) * DOT_PER_MM)); assert_is_close!(profile.bottom.size, Size::splat(18.29) * DOT_PER_MM); assert_is_close!(profile.bottom.radius, Length::new(0.38) * DOT_PER_MM); @@ -750,7 +795,7 @@ mod tests { let expected = [ 0.0, 0.76, 1.52, 2.28, 3.18, 4.84, 6.5, 8.16, 9.82, 11.48, 13.14, ] - .map(|e| e * DOT_PER_MM.0); + .map(|e| Length::::new(e) * DOT_PER_MM); for (e, r) in expected.iter().zip(profile.text_height.0.iter()) { assert_is_close!(e, r); } @@ -768,17 +813,32 @@ mod tests { SideOffsets::new(1.185, 1.18, 1.185, 1.18), SideOffsets::new(1.185, 1.18, 1.185, 1.18), ] - .map(|e| e * DOT_PER_MM.0); + .map(|e| e * DOT_PER_MM); for (e, r) in expected.iter().zip(profile.text_margin.0.iter()) { assert_is_close!(e, r); } assert_matches!(profile.homing.default, Homing::Scoop); - assert_is_close!(profile.homing.scoop.depth, Length::::new(1.5)); - assert_is_close!(profile.homing.bar.size, Size::::new(3.85, 0.4)); - assert_is_close!(profile.homing.bar.y_offset, Length::::new(5.05)); - assert_is_close!(profile.homing.bump.diameter, Length::::new(0.4)); - assert_is_close!(profile.homing.bump.y_offset, Length::::new(-0.2)); + assert_is_close!( + profile.homing.scoop.depth, + Length::::new(1.5) * DOT_PER_MM + ); + assert_is_close!( + profile.homing.bar.size, + Size::::new(3.85, 0.4) * DOT_PER_MM + ); + assert_is_close!( + profile.homing.bar.y_offset, + Length::::new(5.05) * DOT_PER_MM + ); + assert_is_close!( + profile.homing.bump.diameter, + Length::::new(0.4) * DOT_PER_MM + ); + assert_is_close!( + profile.homing.bump.y_offset, + Length::::new(-0.2) * DOT_PER_MM + ); } #[cfg(feature = "json")] @@ -833,7 +893,7 @@ mod tests { fn test_profile_default() { let profile = Profile::default(); - assert_matches!(profile.typ, Type::Cylindrical { depth } if depth.is_close(1.0)); + assert_matches!(profile.typ, Type::Cylindrical { depth } if depth.is_close(Length::::new(1.0) * DOT_PER_MM)); assert_is_close!(profile.bottom.size, Size::splat(0.950) * DOT_PER_UNIT); assert_is_close!(profile.bottom.radius, Length::new(0.065) * DOT_PER_UNIT);