keyset_profile/
lib.rs

1#![warn(
2    missing_docs,
3    clippy::all,
4    clippy::correctness,
5    clippy::suspicious,
6    clippy::style,
7    clippy::complexity,
8    clippy::perf,
9    clippy::pedantic,
10    clippy::cargo,
11    clippy::nursery
12)]
13#![allow(
14    missing_docs, // TODO
15    clippy::missing_errors_doc, // TODO
16    clippy::missing_panics_doc, // TODO
17    clippy::suboptimal_flops // Optimiser is pretty good, and mul_add is pretty ugly
18)]
19
20#[cfg(feature = "serde")]
21mod de;
22
23use std::collections::HashMap;
24use std::{array, iter};
25
26use geom::{Insets, Point, Rect, RoundRect, Size, Vec2};
27use interp::interp_array;
28use itertools::Itertools;
29use key::Homing;
30
31#[derive(Debug, Clone, Copy)]
32#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
33#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "kebab-case"))]
34pub enum Type {
35    Cylindrical { depth: f64 },
36    Spherical { depth: f64 },
37    Flat,
38}
39
40impl Type {
41    // 1.0mm is approx the depth of OEM profile
42    pub const DEFAULT: Self = Self::Cylindrical { depth: 1.0 };
43
44    #[must_use]
45    pub const fn depth(self) -> f64 {
46        match self {
47            Self::Cylindrical { depth } | Self::Spherical { depth } => depth,
48            Self::Flat => 0.0,
49        }
50    }
51}
52
53impl Default for Type {
54    fn default() -> Self {
55        Self::DEFAULT
56    }
57}
58
59#[derive(Debug, Clone, Copy)]
60#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
61pub struct ScoopProps {
62    pub depth: f64,
63}
64
65#[derive(Debug, Clone, Copy)]
66pub struct BarProps {
67    pub size: Size,
68    pub y_offset: f64,
69}
70
71#[derive(Debug, Clone, Copy)]
72pub struct BumpProps {
73    pub diameter: f64,
74    pub y_offset: f64,
75}
76
77#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
78#[cfg_attr(feature = "serde", serde(remote = "Homing", rename_all = "kebab-case"))]
79pub enum HomingDef {
80    #[cfg_attr(feature = "serde", serde(alias = "deep-dish", alias = "dish"))]
81    Scoop,
82    #[cfg_attr(feature = "serde", serde(alias = "line"))]
83    Bar,
84    #[cfg_attr(
85        feature = "serde",
86        serde(alias = "nub", alias = "dot", alias = "nipple")
87    )]
88    Bump,
89}
90
91#[derive(Debug, Clone, Copy)]
92#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
93pub struct HomingProps {
94    #[cfg_attr(feature = "serde", serde(with = "HomingDef"))]
95    pub default: Homing,
96    pub scoop: ScoopProps,
97    pub bar: BarProps,
98    pub bump: BumpProps,
99}
100
101impl HomingProps {
102    pub const DEFAULT: Self = Self {
103        default: Homing::Bar,
104        scoop: ScoopProps {
105            depth: 2.0 * Type::DEFAULT.depth(), // 2x the regular depth
106        },
107        bar: BarProps {
108            size: Size::new(3.81, 0.51), // = 0.15in, 0.02in
109            y_offset: 6.35,              // = 0.25in
110        },
111        bump: BumpProps {
112            diameter: 0.51, // = 0.02in
113            y_offset: 0.0,
114        },
115    };
116}
117
118impl Default for HomingProps {
119    fn default() -> Self {
120        Self::DEFAULT
121    }
122}
123
124#[derive(Debug, Clone)]
125pub struct TextHeight([f64; Self::NUM_HEIGHTS]);
126
127impl TextHeight {
128    const NUM_HEIGHTS: usize = 10;
129
130    // From: https://github.com/ijprest/keyboard-layout-editor/blob/d2945e5b0a9cdfc7cc9bb225839192298d82a66d/kb.css#L113
131    // TODO (6.0 + 2.0 * (i as f64)) * (1e3 / 72.)
132    pub const DEFAULT: Self = Self([
133        83.333_333_333,
134        111.111_111_111,
135        138.888_888_889,
136        166.666_666_667,
137        194.444_444_444,
138        222.222_222_222,
139        250.0,
140        277.777_777_778,
141        305.555_555_556,
142        333.333_333_333,
143    ]);
144
145    #[must_use]
146    pub fn new(heights: &HashMap<usize, f64>) -> Self {
147        if heights.is_empty() {
148            Self::default()
149        } else {
150            let (index, height): (Vec<_>, Vec<_>) = {
151                iter::once((0., 0.))
152                    .chain(
153                        #[allow(clippy::cast_precision_loss)]
154                        heights
155                            .iter()
156                            .sorted_by_key(|(&i, _)| i)
157                            .map(|(&i, &h)| (i as f64, h)),
158                    )
159                    .unzip()
160            };
161            #[allow(clippy::cast_precision_loss)]
162            let all_indeces = array::from_fn(|i| i as f64);
163            Self(interp_array(&index, &height, &all_indeces))
164        }
165    }
166
167    #[must_use]
168    pub const fn get(&self, size_index: usize) -> f64 {
169        if size_index < self.0.len() {
170            self.0[size_index]
171        } else {
172            self.0[self.0.len() - 1]
173        }
174    }
175}
176
177impl Default for TextHeight {
178    fn default() -> Self {
179        Self::DEFAULT
180    }
181}
182
183#[derive(Debug, Clone)]
184pub struct TextMargin([Insets; Self::NUM_RECTS]);
185
186impl TextMargin {
187    const NUM_RECTS: usize = 10;
188
189    pub const DEFAULT: Self = Self([Insets::uniform(-50.0); Self::NUM_RECTS]);
190
191    #[must_use]
192    pub fn new(insets: &HashMap<usize, Insets>) -> Self {
193        if insets.is_empty() {
194            Self::default()
195        } else {
196            // Note this unwrap will not panic because we know rects is not empty at this stage
197            let max_rect = insets[insets.keys().max().unwrap()];
198
199            // TODO clean up this logic
200            // For each font size where the alignment rectangle isn't set, the rectangle of the
201            // next largest rect is used, so we need to scan in reverse to carry the back the next
202            // largest rect.
203            let insets: Vec<_> = {
204                let tmp = (0..Self::NUM_RECTS)
205                    .rev()
206                    .scan(max_rect, |prev, i| {
207                        if let Some(&value) = insets.get(&i) {
208                            *prev = value;
209                        }
210                        Some(*prev)
211                    })
212                    .collect_vec();
213                tmp.into_iter().rev().collect()
214            };
215
216            Self(insets.try_into().unwrap())
217        }
218    }
219
220    #[must_use]
221    pub const fn get(&self, size_index: usize) -> Insets {
222        if size_index < self.0.len() {
223            self.0[size_index]
224        } else {
225            self.0[self.0.len() - 1]
226        }
227    }
228}
229
230impl Default for TextMargin {
231    fn default() -> Self {
232        Self::DEFAULT
233    }
234}
235
236#[derive(Debug, Clone)]
237pub struct TopSurface {
238    pub size: Size,
239    pub radius: f64,
240    pub y_offset: f64,
241}
242
243impl TopSurface {
244    pub const DEFAULT: Self = Self {
245        size: Size::new(660.0, 735.0),
246        radius: 65.0,
247        y_offset: -77.5,
248    };
249
250    pub(crate) fn rect(&self) -> Rect {
251        Rect::from_center_size(Point::new(500., 500. + self.y_offset), self.size)
252    }
253
254    pub(crate) fn round_rect(&self) -> RoundRect {
255        RoundRect::from_rect(self.rect(), Vec2::new(self.radius, self.radius))
256    }
257}
258
259impl Default for TopSurface {
260    fn default() -> Self {
261        Self::DEFAULT
262    }
263}
264
265#[derive(Debug, Clone)]
266pub struct BottomSurface {
267    pub size: Size,
268    pub radius: f64,
269}
270
271impl BottomSurface {
272    pub const DEFAULT: Self = Self {
273        size: Size::new(950.0, 950.0),
274        radius: 65.0,
275    };
276
277    pub(crate) fn rect(&self) -> Rect {
278        Rect::from_center_size(Point::new(500., 500.), self.size)
279    }
280
281    pub(crate) fn round_rect(&self) -> RoundRect {
282        RoundRect::from_rect(self.rect(), Vec2::new(self.radius, self.radius))
283    }
284}
285
286impl Default for BottomSurface {
287    fn default() -> Self {
288        Self::DEFAULT
289    }
290}
291
292#[derive(Debug, Clone)]
293pub struct Profile {
294    pub typ: Type,
295    pub bottom: BottomSurface,
296    pub top: TopSurface,
297    pub text_margin: TextMargin,
298    pub text_height: TextHeight,
299    pub homing: HomingProps,
300}
301
302impl Profile {
303    pub const DEFAULT: Self = Self {
304        typ: Type::DEFAULT,
305        bottom: BottomSurface::DEFAULT,
306        top: TopSurface::DEFAULT,
307        text_margin: TextMargin::DEFAULT,
308        text_height: TextHeight::DEFAULT,
309        homing: HomingProps::DEFAULT,
310    };
311
312    #[cfg(feature = "toml")]
313    pub fn from_toml(s: &str) -> de::Result<Self> {
314        toml::from_str(s).map_err(de::Error::from)
315    }
316
317    #[cfg(feature = "json")]
318    pub fn from_json(s: &str) -> de::Result<Self> {
319        serde_json::from_str(s).map_err(de::Error::from)
320    }
321
322    pub fn top_with_size(&self, size: impl Into<Size>) -> RoundRect {
323        let top_rect = self.top.round_rect();
324        top_rect.with_size(top_rect.size() + 1e3 * (size.into() - Size::new(1., 1.)))
325    }
326
327    pub fn top_with_rect(&self, rect: impl Into<Rect>) -> RoundRect {
328        let rect = rect.into();
329        let result = self.top_with_size(rect.size());
330        result.with_origin(result.origin() + 1e3 * rect.origin().to_vec2())
331    }
332
333    pub fn bottom_with_size(&self, size: impl Into<Size>) -> RoundRect {
334        let bottom_rect = self.bottom.round_rect();
335        bottom_rect.with_size(bottom_rect.size() + 1e3 * (size.into() - Size::new(1., 1.)))
336    }
337}
338
339impl Default for Profile {
340    fn default() -> Self {
341        Self::DEFAULT
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use assert_approx_eq::assert_approx_eq;
348    use assert_matches::assert_matches;
349    use unindent::unindent;
350
351    use super::*;
352
353    #[test]
354    fn test_profile_type_depth() {
355        assert_eq!(Type::Cylindrical { depth: 1. }.depth(), 1.);
356        assert_eq!(Type::Spherical { depth: 0.5 }.depth(), 0.5);
357        assert_eq!(Type::Flat.depth(), 0.);
358    }
359
360    #[test]
361    fn test_profile_type_default() {
362        assert_matches!(Type::default(), Type::Cylindrical { depth } if depth == 1.);
363    }
364
365    #[test]
366    fn test_homing_props_default() {
367        assert_matches!(HomingProps::default().default, Homing::Bar);
368        assert_eq!(HomingProps::default().scoop.depth, 2.);
369        assert_eq!(HomingProps::default().bar.size, Size::new(3.81, 0.51));
370        assert_eq!(HomingProps::default().bar.y_offset, 6.35);
371        assert_eq!(HomingProps::default().bump.diameter, 0.51);
372        assert_eq!(HomingProps::default().bump.y_offset, 0.);
373    }
374
375    #[test]
376    fn test_text_height_new() {
377        let expected: [_; 10] = array::from_fn(|i| (6. + 2. * (i as f64)) * (1e3 / 72.));
378        let result = TextHeight::new(&HashMap::new()).0;
379
380        assert_eq!(expected.len(), result.len());
381
382        for (e, r) in expected.iter().zip(result.iter()) {
383            assert_approx_eq!(e, r);
384        }
385
386        let expected = [0., 60., 120., 180., 190., 210., 230., 280., 330., 380.];
387        let result = TextHeight::new(&HashMap::from([
388            (1, 60.0),
389            (3, 180.0),
390            (4, 190.0),
391            (6, 230.0),
392            (9, 380.0),
393        ]))
394        .0;
395
396        assert_eq!(expected.len(), result.len());
397
398        for (e, r) in expected.iter().zip(result.iter()) {
399            assert_approx_eq!(e, r);
400        }
401    }
402
403    #[test]
404    fn test_text_height_get() {
405        let heights = TextHeight::new(&HashMap::from([
406            (1, 3.0),
407            (3, 9.0),
408            (4, 9.5),
409            (6, 11.5),
410            (9, 19.0),
411        ]));
412        assert_approx_eq!(heights.get(5), 10.5);
413        assert_approx_eq!(heights.get(23), 19.);
414    }
415
416    #[test]
417    fn test_text_height_default() {
418        let heights = TextHeight::default();
419
420        for (i, h) in heights.0.into_iter().enumerate() {
421            assert_approx_eq!(h, (6. + 2. * (i as f64)) * (1e3 / 72.));
422        }
423    }
424
425    #[test]
426    fn test_text_margin_new() {
427        let expected = vec![Insets::uniform(-50.); 10];
428        let result = TextMargin::new(&HashMap::new()).0;
429
430        assert_eq!(expected.len(), result.len());
431
432        for (e, r) in expected.iter().zip(result.iter()) {
433            assert_approx_eq!(e.size().width, r.size().width);
434            assert_approx_eq!(e.size().height, r.size().height);
435        }
436
437        let expected = vec![
438            Insets::uniform(0.),
439            Insets::uniform(0.),
440            Insets::uniform(0.),
441            Insets::uniform(-50.),
442            Insets::uniform(-50.),
443            Insets::uniform(-50.),
444            Insets::uniform(-100.),
445            Insets::uniform(-100.),
446            Insets::uniform(-100.),
447            Insets::uniform(-100.),
448        ];
449        let result = TextMargin::new(&HashMap::from([
450            (2, Insets::uniform(0.0)),
451            (5, Insets::uniform(-50.0)),
452            (7, Insets::uniform(-100.0)),
453        ]))
454        .0;
455
456        assert_eq!(expected.len(), result.len());
457
458        for (e, r) in expected.iter().zip(result.iter()) {
459            assert_approx_eq!(e.size().width, r.size().width);
460            assert_approx_eq!(e.size().height, r.size().height);
461        }
462    }
463
464    #[test]
465    fn test_text_margin_get() {
466        let margin = TextMargin::new(&HashMap::from([
467            (2, Insets::uniform(0.0)),
468            (5, Insets::uniform(-50.0)),
469            (7, Insets::uniform(-100.0)),
470        ]));
471
472        let inset = margin.get(2);
473        assert_approx_eq!(inset.size().width, 0.0);
474        assert_approx_eq!(inset.size().height, 0.0);
475
476        let inset = margin.get(62);
477        assert_approx_eq!(inset.size().width, -200.0);
478        assert_approx_eq!(inset.size().height, -200.0);
479    }
480
481    #[test]
482    fn test_text_margin_default() {
483        let margin = TextMargin::default();
484
485        for inset in margin.0.into_iter() {
486            assert_approx_eq!(inset.size().width, -100.0);
487            assert_approx_eq!(inset.size().height, -100.0);
488        }
489    }
490
491    #[test]
492    fn test_top_surface_rect() {
493        let surf = TopSurface::default();
494        assert_eq!(surf.rect().origin(), Point::new(170., 55.),);
495        assert_eq!(surf.rect().size(), Size::new(660., 735.),);
496    }
497
498    #[test]
499    fn test_top_surface_round_rect() {
500        let surf = TopSurface::default();
501        assert_eq!(surf.round_rect().origin(), Point::new(170., 55.),);
502        assert_eq!(surf.round_rect().size(), Size::new(660., 735.),);
503        assert_eq!(surf.round_rect().radii(), Vec2::new(65., 65.),);
504    }
505
506    #[test]
507    fn test_top_surface_default() {
508        let surf = TopSurface::default();
509        assert_eq!(surf.size, Size::new(660., 735.));
510        assert_eq!(surf.radius, 65.);
511        assert_eq!(surf.y_offset, -77.5);
512    }
513
514    #[test]
515    fn test_bottom_surface_rect() {
516        let surf = BottomSurface::default();
517        assert_eq!(surf.rect().origin(), Point::new(25., 25.),);
518        assert_eq!(surf.rect().size(), Size::new(950., 950.),);
519    }
520
521    #[test]
522    fn test_bottom_surface_round_rect() {
523        let surf = BottomSurface::default();
524        assert_eq!(surf.round_rect().origin(), Point::new(25., 25.),);
525        assert_eq!(surf.round_rect().size(), Size::new(950., 950.),);
526        assert_eq!(surf.round_rect().radii(), Vec2::new(65., 65.),);
527    }
528
529    #[test]
530    fn test_bottom_surface_default() {
531        let surf = BottomSurface::default();
532        assert_eq!(surf.size, Size::new(950., 950.));
533        assert_eq!(surf.radius, 65.);
534    }
535
536    #[cfg(feature = "toml")]
537    #[test]
538    fn test_profile_from_toml() {
539        let profile = Profile::from_toml(&unindent(
540            r#"
541            type = 'cylindrical'
542            depth = 0.5
543
544            [bottom]
545            width = 18.29
546            height = 18.29
547            radius = 0.38
548
549            [top]
550            width = 11.81
551            height = 13.91
552            radius = 1.52
553            y-offset = -1.62
554
555            [legend.5]
556            size = 4.84
557            width = 9.45
558            height = 11.54
559            y-offset = 0
560
561            [legend.4]
562            size = 3.18
563            width = 9.53
564            height = 9.56
565            y-offset = 0.40
566
567            [legend.3]
568            size = 2.28
569            width = 9.45
570            height = 11.30
571            y-offset = -0.12
572
573            [homing]
574            default = 'scoop'
575            scoop = { depth = 1.5 }
576            bar = { width = 3.85, height = 0.4, y-offset = 5.05 }
577            bump = { diameter = 0.4, y-offset = -0.2 }
578        "#,
579        ))
580        .unwrap();
581
582        assert!(matches!(profile.typ, Type::Cylindrical { depth } if f64::abs(depth - 0.5) < 1e-6));
583
584        assert_approx_eq!(profile.bottom.size.width, 960.0, 0.5);
585        assert_approx_eq!(profile.bottom.size.height, 960.0, 0.5);
586        assert_approx_eq!(profile.bottom.radius, 20.0, 0.5);
587
588        assert_approx_eq!(profile.top.size.width, 620.0, 0.5);
589        assert_approx_eq!(profile.top.size.height, 730.0, 0.5);
590        assert_approx_eq!(profile.top.radius, 80., 0.5);
591
592        assert_eq!(profile.text_height.0.len(), 10);
593        let expected = vec![0., 40., 80., 120., 167., 254., 341., 428., 515., 603., 690.];
594        for (e, r) in expected.iter().zip(profile.text_height.0.iter()) {
595            assert_approx_eq!(e, r, 0.5);
596        }
597
598        assert_eq!(profile.text_margin.0.len(), 10);
599        let expected = vec![
600            Insets::new(-62., -62., -62., -75.),
601            Insets::new(-62., -62., -62., -75.),
602            Insets::new(-62., -62., -62., -75.),
603            Insets::new(-62., -62., -62., -75.),
604            Insets::new(-60., -135., -60., -93.),
605            Insets::new(-62., -62., -62., -62.),
606            Insets::new(-62., -62., -62., -62.),
607            Insets::new(-62., -62., -62., -62.),
608            Insets::new(-62., -62., -62., -62.),
609            Insets::new(-62., -62., -62., -62.),
610        ];
611        for (e, r) in expected.iter().zip(profile.text_margin.0.iter()) {
612            assert_approx_eq!(e.size().width, r.size().width, 0.5);
613            assert_approx_eq!(e.size().height, r.size().height, 0.5);
614        }
615
616        assert_matches!(profile.homing.default, Homing::Scoop);
617        assert_approx_eq!(profile.homing.scoop.depth, 1.5);
618        assert_approx_eq!(profile.homing.bar.size.width, 202.0, 0.5);
619        assert_approx_eq!(profile.homing.bar.size.height, 21.0, 0.5);
620        assert_approx_eq!(profile.homing.bar.y_offset, 265., 0.5);
621        assert_approx_eq!(profile.homing.bump.diameter, 21., 0.5);
622        assert_approx_eq!(profile.homing.bump.y_offset, -10., 0.5);
623
624        let result = Profile::from_toml("null");
625        assert!(result.is_err());
626        assert_eq!(
627            format!("{}", result.unwrap_err()),
628            unindent(
629                r#"TOML parse error at line 1, column 5
630                  |
631                1 | null
632                  |     ^
633                expected `.`, `=`
634                "#
635            ),
636        )
637    }
638
639    #[cfg(feature = "json")]
640    #[test]
641    fn test_profile_from_json() {
642        let profile = Profile::from_json(&unindent(
643            r#"{
644                "type": "cylindrical",
645                "depth": 0.5,
646
647                "bottom": {
648                    "width": 18.29,
649                    "height": 18.29,
650                    "radius": 0.38
651                },
652
653                "top": {
654                    "width": 11.81,
655                    "height": 13.91,
656                    "radius": 1.52,
657                    "y-offset": -1.62
658                },
659
660                "legend": {
661                    "5": {
662                        "size": 4.84,
663                        "width": 9.45,
664                        "height": 11.54,
665                        "y-offset": 0
666                    },
667                    "4": {
668                        "size": 3.18,
669                        "width": 9.53,
670                        "height": 9.56,
671                        "y-offset": 0.40
672                    },
673                    "3": {
674                        "size": 2.28,
675                        "width": 9.45,
676                        "height": 11.30,
677                        "y-offset": -0.12
678                    }
679                },
680
681                "homing": {
682                    "default": "scoop",
683                    "scoop": {
684                        "depth": 1.5
685                    },
686                    "bar": {
687                        "width": 3.85,
688                        "height": 0.4,
689                        "y-offset": 5.05
690                    },
691                    "bump": {
692                        "diameter": 0.4,
693                        "y-offset": -0.2
694                    }
695                }
696            }"#,
697        ))
698        .unwrap();
699
700        assert!(matches!(profile.typ, Type::Cylindrical { depth } if f64::abs(depth - 0.5) < 1e-6));
701
702        assert_approx_eq!(profile.bottom.size.width, 960.0, 0.5);
703        assert_approx_eq!(profile.bottom.size.height, 960.0, 0.5);
704        assert_approx_eq!(profile.bottom.radius, 20.0, 0.5);
705
706        assert_approx_eq!(profile.top.size.width, 620.0, 0.5);
707        assert_approx_eq!(profile.top.size.height, 730.0, 0.5);
708        assert_approx_eq!(profile.top.radius, 80., 0.5);
709
710        assert_eq!(profile.text_height.0.len(), 10);
711        let expected = vec![0., 40., 80., 120., 167., 254., 341., 428., 515., 603., 690.];
712        for (e, r) in expected.iter().zip(profile.text_height.0.iter()) {
713            assert_approx_eq!(e, r, 0.5);
714        }
715
716        assert_eq!(profile.text_margin.0.len(), 10);
717        let expected = vec![
718            Insets::new(-62., -62., -62., -75.),
719            Insets::new(-62., -62., -62., -75.),
720            Insets::new(-62., -62., -62., -75.),
721            Insets::new(-62., -62., -62., -75.),
722            Insets::new(-60., -135., -60., -93.),
723            Insets::new(-62., -62., -62., -62.),
724            Insets::new(-62., -62., -62., -62.),
725            Insets::new(-62., -62., -62., -62.),
726            Insets::new(-62., -62., -62., -62.),
727            Insets::new(-62., -62., -62., -62.),
728        ];
729        for (e, r) in expected.iter().zip(profile.text_margin.0.iter()) {
730            assert_approx_eq!(e.size().width, r.size().width, 0.5);
731            assert_approx_eq!(e.size().height, r.size().height, 0.5);
732        }
733
734        assert_matches!(profile.homing.default, Homing::Scoop);
735        assert_approx_eq!(profile.homing.scoop.depth, 1.5);
736        assert_approx_eq!(profile.homing.bar.size.width, 202.0, 0.5);
737        assert_approx_eq!(profile.homing.bar.size.height, 21.0, 0.5);
738        assert_approx_eq!(profile.homing.bar.y_offset, 265., 0.5);
739        assert_approx_eq!(profile.homing.bump.diameter, 21., 0.5);
740        assert_approx_eq!(profile.homing.bump.y_offset, -10., 0.5);
741
742        let result = Profile::from_json("null");
743        assert!(result.is_err());
744        assert_eq!(
745            format!("{}", result.unwrap_err()),
746            "invalid type: null, expected struct RawProfileData at line 1 column 4"
747        )
748    }
749
750    #[test]
751    fn test_profile_with_size() {
752        let profile = Profile::default();
753
754        let top = profile.top_with_size((1.0, 1.0));
755        assert_approx_eq!(top.origin().x, 500.0 - profile.top.size.width / 2.0);
756        assert_approx_eq!(
757            top.origin().y,
758            500.0 - profile.top.size.height / 2.0 + profile.top.y_offset
759        );
760        assert_approx_eq!(top.size().width, profile.top.size.width);
761        assert_approx_eq!(top.size().height, profile.top.size.height);
762
763        let bottom = profile.bottom_with_size((1.0, 1.0));
764        assert_approx_eq!(bottom.origin().x, 500.0 - profile.bottom.size.width / 2.0);
765        assert_approx_eq!(bottom.origin().y, 500.0 - profile.bottom.size.height / 2.0);
766        assert_approx_eq!(bottom.size().width, profile.bottom.size.width);
767        assert_approx_eq!(bottom.size().height, profile.bottom.size.height);
768
769        let top = profile.top_with_size((3.0, 2.0));
770        assert_approx_eq!(top.origin().x, 500.0 - profile.top.size.width / 2.0);
771        assert_approx_eq!(
772            top.origin().y,
773            500.0 - profile.top.size.height / 2.0 + profile.top.y_offset
774        );
775        assert_approx_eq!(top.size().width, profile.top.size.width + 2e3);
776        assert_approx_eq!(top.size().height, profile.top.size.height + 1e3);
777
778        let bottom = profile.bottom_with_size((3.0, 2.0));
779        assert_approx_eq!(bottom.origin().x, 500.0 - profile.bottom.size.width / 2.0);
780        assert_approx_eq!(bottom.origin().y, 500.0 - profile.bottom.size.height / 2.0);
781        assert_approx_eq!(bottom.size().width, profile.bottom.size.width + 2e3);
782        assert_approx_eq!(bottom.size().height, profile.bottom.size.height + 1e3);
783    }
784
785    #[test]
786    fn test_profile_default() {
787        let profile = Profile::default();
788
789        assert_matches!(profile.typ, Type::Cylindrical { depth } if depth == 1.);
790
791        assert_approx_eq!(profile.bottom.size.width, 950.0);
792        assert_approx_eq!(profile.bottom.size.height, 950.0);
793        assert_approx_eq!(profile.bottom.radius, 65.);
794
795        assert_approx_eq!(profile.top.size.width, 660.0);
796        assert_approx_eq!(profile.top.size.height, 735.0);
797        assert_approx_eq!(profile.top.radius, 65.);
798        assert_approx_eq!(profile.top.y_offset, -77.5);
799
800        assert_eq!(profile.text_height.0.len(), 10);
801        let expected = TextHeight::default();
802        for (e, r) in expected.0.iter().zip(profile.text_height.0.iter()) {
803            assert_approx_eq!(e, r);
804        }
805
806        assert_eq!(profile.text_margin.0.len(), 10);
807        let expected = TextMargin::default();
808        for (e, r) in expected.0.iter().zip(profile.text_margin.0.iter()) {
809            assert_approx_eq!(e.size().width, r.size().width, 0.5);
810            assert_approx_eq!(e.size().height, r.size().height, 0.5);
811        }
812    }
813}