Skip to main content

hpr_design/
material.rs

1//! Materials: a name and a density per volume, per area or per length.
2//!
3//! Solid parts (tubes, nose cones, fins, rings) take a **bulk** density, fabrics (parachute
4//! canopies, streamers) a **surface** density, and cords (shroud lines, shock cords) a **line**
5//! density. A design stores the material's values, not a reference into a library, so it stays
6//! complete offline; [`crate::materials`] lists built-in values with their sources.
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::DesignError;
11
12/// A density, with its units in the variant.
13#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
14#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
15pub enum Density {
16    /// Mass per volume.
17    Bulk {
18        /// kg/m³.
19        kg_m3: f64,
20    },
21    /// Mass per area, for sheets and fabrics.
22    Surface {
23        /// kg/m².
24        kg_m2: f64,
25    },
26    /// Mass per length, for cords and lines.
27    Line {
28        /// kg/m.
29        kg_m: f64,
30    },
31}
32
33impl Density {
34    /// The kind's name, for messages.
35    pub fn kind_name(&self) -> &'static str {
36        match self {
37            Self::Bulk { .. } => "bulk",
38            Self::Surface { .. } => "surface",
39            Self::Line { .. } => "line",
40        }
41    }
42}
43
44/// A named material.
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
46#[serde(deny_unknown_fields)]
47pub struct Material {
48    /// Name, for display and for matching imported designs.
49    pub name: String,
50    /// Density.
51    pub density: Density,
52}
53
54impl Material {
55    /// A bulk material of `kg_m3`.
56    pub fn bulk(name: impl Into<String>, kg_m3: f64) -> Self {
57        Self {
58            name: name.into(),
59            density: Density::Bulk { kg_m3 },
60        }
61    }
62
63    /// A surface material of `kg_m2`.
64    pub fn surface(name: impl Into<String>, kg_m2: f64) -> Self {
65        Self {
66            name: name.into(),
67            density: Density::Surface { kg_m2 },
68        }
69    }
70
71    /// A line material of `kg_m`.
72    pub fn line(name: impl Into<String>, kg_m: f64) -> Self {
73        Self {
74            name: name.into(),
75            density: Density::Line { kg_m },
76        }
77    }
78
79    /// The value of a density of the kind `expected` names, for `part`.
80    fn value(&self, part: &'static str, expected: &'static str) -> Result<f64, DesignError> {
81        let value = match (self.density, expected) {
82            (Density::Bulk { kg_m3 }, "bulk") => kg_m3,
83            (Density::Surface { kg_m2 }, "surface") => kg_m2,
84            (Density::Line { kg_m }, "line") => kg_m,
85            _ => {
86                return Err(DesignError::MaterialKind {
87                    part,
88                    material: self.name.clone(),
89                    expected,
90                    actual: self.density.kind_name(),
91                });
92            }
93        };
94        if value.is_finite() && value >= 0.0 {
95            Ok(value)
96        } else {
97            Err(DesignError::Domain {
98                what: "material density",
99                value,
100            })
101        }
102    }
103
104    /// The bulk density for `part`, kg/m³.
105    ///
106    /// # Errors
107    ///
108    /// [`DesignError::MaterialKind`] if the material isn't bulk, [`DesignError::Domain`] if its
109    /// density is negative or not finite.
110    pub fn bulk_kg_m3(&self, part: &'static str) -> Result<f64, DesignError> {
111        self.value(part, "bulk")
112    }
113
114    /// The surface density for `part`, kg/m²; errors as [`Material::bulk_kg_m3`].
115    ///
116    /// # Errors
117    ///
118    /// As [`Material::bulk_kg_m3`].
119    pub fn surface_kg_m2(&self, part: &'static str) -> Result<f64, DesignError> {
120        self.value(part, "surface")
121    }
122
123    /// The line density for `part`, kg/m; errors as [`Material::bulk_kg_m3`].
124    ///
125    /// # Errors
126    ///
127    /// As [`Material::bulk_kg_m3`].
128    pub fn line_kg_m(&self, part: &'static str) -> Result<f64, DesignError> {
129        self.value(part, "line")
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn kinds_are_checked_and_serialized_with_their_units() {
139        let ply = Material::bulk("Birch plywood", 630.0);
140        assert_eq!(ply.bulk_kg_m3("fin set").unwrap(), 630.0);
141        assert_eq!(
142            ply.surface_kg_m2("parachute canopy"),
143            Err(DesignError::MaterialKind {
144                part: "parachute canopy",
145                material: "Birch plywood".to_owned(),
146                expected: "surface",
147                actual: "bulk",
148            })
149        );
150        let json = serde_json::to_string(&Material::line("Kevlar", 0.0097)).unwrap();
151        assert_eq!(
152            json,
153            r#"{"name":"Kevlar","density":{"kind":"line","kg_m":0.0097}}"#
154        );
155        assert!(Material::bulk("bad", -1.0).bulk_kg_m3("tube").is_err());
156        assert!(
157            serde_json::from_str::<Material>(r#"{"name":"x","density":{"kind":"bulk","kg_m2":1}}"#)
158                .is_err()
159        );
160    }
161}