diff --git a/crates/model/src/datums.rs b/crates/model/src/datums.rs index b830ff7..3b5460e 100644 --- a/crates/model/src/datums.rs +++ b/crates/model/src/datums.rs @@ -174,7 +174,8 @@ impl Model { /// The definitions that refer to a datum (the partitions that cut by it), as `kind \`name\`` /// strings. pub fn datum_users(&self, name: &str) -> Vec { - self.partitions.iter().filter(|p| p.datum == name).map(|p| format!("partition `{}`", p.name)).collect() + let partitions = self.partitions.iter().filter(|p| p.datum == name).map(|p| format!("partition `{}`", p.name)); + partitions.chain(self.face_splits().iter().filter(|s| s.datum == name).map(|s| format!("face split `{}`", s.name))).collect() } /// The partitions, in the order they cut. diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 082ec39..e612296 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -85,7 +85,7 @@ pub use steps::{ }; pub use tie::Tie; pub use units::UnitSystem; -pub use virtual_faces::{FaceMerge, Stitch}; +pub use virtual_faces::{FaceMerge, FaceSplit, Stitch}; /// The largest node or element label. Nastran's GRID and element ids stop at 99 999 999 (eight /// digits in a small field), the tightest of the three formats. @@ -180,6 +180,9 @@ pub struct Model { /// Stitches of imported surfaces, in the order they apply. #[serde(skip_serializing_if = "Vec::is_empty")] stitches: Vec, + /// Face splits along datum planes, in the order they apply (after the merges). + #[serde(skip_serializing_if = "Vec::is_empty")] + face_splits: Vec, } /// A [`Model`] as stored, before it is checked. @@ -236,6 +239,8 @@ struct ModelData { face_merges: Vec, #[serde(default)] stitches: Vec, + #[serde(default)] + face_splits: Vec, } impl TryFrom for Model { @@ -331,6 +336,9 @@ impl TryFrom for Model { for v in d.stitches { m.add_stitch(v)?; } + for v in d.face_splits { + m.add_face_split(v)?; + } Ok(m) } } @@ -465,6 +473,7 @@ impl Model { mesh_plans: self.mesh_plans.clone(), face_merges: self.face_merges.clone(), stitches: self.stitches.clone(), + face_splits: self.face_splits.clone(), } } @@ -515,7 +524,7 @@ impl Model { } /// Counts of each kind of definition, for summaries. - pub fn definition_counts(&self) -> [(&'static str, usize); 18] { + pub fn definition_counts(&self) -> [(&'static str, usize); 19] { [ ("sets", self.sets.len()), ("materials", self.materials.len()), @@ -535,6 +544,7 @@ impl Model { ("seeds", self.seeds.len()), ("face_merges", self.face_merges.len()), ("stitches", self.stitches.len()), + ("face_splits", self.face_splits.len()), ] } } diff --git a/crates/model/src/virtual_faces.rs b/crates/model/src/virtual_faces.rs index b911ac9..151ca13 100644 --- a/crates/model/src/virtual_faces.rs +++ b/crates/model/src/virtual_faces.rs @@ -33,6 +33,59 @@ pub struct FaceMerge { pub into: Option, } +/// Most face splits a model holds. +pub const MAX_FACE_SPLITS: usize = 1024; + +/// A face of a body split along a datum plane for meshing (`split::a|b`). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct FaceSplit { + pub name: String, + pub body: String, + pub face: String, + /// The datum plane it splits along. + pub datum: String, +} + +impl Model { + /// The face splits, in the order they apply (after the merges). + pub fn face_splits(&self) -> &[FaceSplit] { + &self.face_splits + } + + /// Add a face split after the others. + pub fn add_face_split(&mut self, split: FaceSplit) -> Result<()> { + let name = split.name.clone(); + check_name("face split", &name)?; + if self.face_splits.iter().any(|s| s.name == name) { + return Err(Error::Invalid(format!("face split `{name}` already exists"))); + } + if self.face_splits.len() >= MAX_FACE_SPLITS { + return Err(Error::Limit(format!("a model holds at most {MAX_FACE_SPLITS} face splits"))); + } + let invalid = |m: String| context("face split", &name, Error::Invalid(m)); + for n in [&split.body, &split.face] { + if n.trim().is_empty() || n.len() > MAX_GEOMETRY_NAME_LEN { + return Err(invalid(format!("body and face names are 1 to {MAX_GEOMETRY_NAME_LEN} bytes"))); + } + } + match self.datum(&split.datum) { + None => return Err(context("face split", &name, Error::NotFound(format!("datum `{}`", split.datum)))), + Some(d) if d.plane().is_none() => { + return Err(invalid(format!("datum `{}` is a datum {}; a split is along a plane", split.datum, d.kind()))); + } + Some(_) => {} + } + self.face_splits.push(split); + Ok(()) + } + + pub fn remove_face_split(&mut self, name: &str) -> Result { + let i = self.face_splits.iter().position(|s| s.name == name).ok_or_else(|| Error::NotFound(format!("face split `{name}`")))?; + Ok(self.face_splits.remove(i)) + } +} + /// Most stitches a model holds. pub const MAX_STITCHES: usize = 1024; @@ -132,6 +185,23 @@ mod tests { FaceMerge { name: name.into(), body: "Base".into(), faces: faces.iter().map(|f| f.to_string()).collect(), into: None } } + #[test] + fn splits_need_a_datum_plane_and_hold_it() { + let mut m = Model::default(); + m.add_datum("Mid", crate::Datum::Plane { point: [0.0; 3], normal: [1.0, 0.0, 0.0] }).unwrap(); + m.add_datum("P", crate::Datum::Point { point: [0.0; 3] }).unwrap(); + let sp = |n: &str, d: &str| FaceSplit { name: n.into(), body: "Base".into(), face: "F1:+z".into(), datum: d.into() }; + m.add_face_split(sp("S", "Mid")).unwrap(); + for bad in [sp("S", "Mid"), sp("T", "P"), sp("U", "nope"), FaceSplit { face: String::new(), ..sp("V", "Mid") }] { + assert!(m.add_face_split(bad).is_err()); + } + assert!(m.remove_datum("Mid").is_err(), "in use by the split"); + let back: Model = serde_json::from_str(&serde_json::to_string(&m).unwrap()).unwrap(); + assert_eq!(back.face_splits(), m.face_splits()); + m.remove_face_split("S").unwrap(); + m.remove_datum("Mid").unwrap(); + } + #[test] fn stitches_are_kept_in_order_and_checked() { let mut m = Model::default();