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, clippy::missing_errors_doc, clippy::missing_panics_doc, clippy::suboptimal_flops )]
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 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(), },
107 bar: BarProps {
108 size: Size::new(3.81, 0.51), y_offset: 6.35, },
111 bump: BumpProps {
112 diameter: 0.51, 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 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 let max_rect = insets[insets.keys().max().unwrap()];
198
199 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}