model: face splits along datum planes (#508)

A datum plane a split uses is in use, as one a partition cuts by.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 committed 2026-10-16 15:14:54 -04:00
1 parent c5862b61c9
commit 99c4a2628f
3 files changed
+84 -3

No files matched your search

+2 -1
View File
@@ -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<String> {
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.
+12 -2
View File
@@ -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<Stitch>,
/// Face splits along datum planes, in the order they apply (after the merges).
#[serde(skip_serializing_if = "Vec::is_empty")]
face_splits: Vec<FaceSplit>,
}
/// A [`Model`] as stored, before it is checked.
@@ -236,6 +239,8 @@ struct ModelData {
face_merges: Vec<FaceMerge>,
#[serde(default)]
stitches: Vec<Stitch>,
#[serde(default)]
face_splits: Vec<FaceSplit>,
}
impl TryFrom<ModelData> for Model {
@@ -331,6 +336,9 @@ impl TryFrom<ModelData> 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()),
]
}
}
+70
View File
@@ -33,6 +33,59 @@ pub struct FaceMerge {
pub into: Option<String>,
}
/// 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:<name>: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<FaceSplit> {
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();