forked from 1b-agent/fieldcraft-upstream
model: stitches of imported surfaces, kept in order (#507)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
8cd7b43c35
commit
12b3128f53
2 files changed
+75
-2
No files matched your search
+12
-2
@@ -85,7 +85,7 @@ pub use steps::{
|
|||||||
};
|
};
|
||||||
pub use tie::Tie;
|
pub use tie::Tie;
|
||||||
pub use units::UnitSystem;
|
pub use units::UnitSystem;
|
||||||
pub use virtual_faces::FaceMerge;
|
pub use virtual_faces::{FaceMerge, Stitch};
|
||||||
|
|
||||||
/// The largest node or element label. Nastran's GRID and element ids stop at 99 999 999 (eight
|
/// 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.
|
/// digits in a small field), the tightest of the three formats.
|
||||||
@@ -177,6 +177,9 @@ pub struct Model {
|
|||||||
/// Face merges (virtual topology), in the order they apply.
|
/// Face merges (virtual topology), in the order they apply.
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
face_merges: Vec<FaceMerge>,
|
face_merges: Vec<FaceMerge>,
|
||||||
|
/// Stitches of imported surfaces, in the order they apply.
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
stitches: Vec<Stitch>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [`Model`] as stored, before it is checked.
|
/// A [`Model`] as stored, before it is checked.
|
||||||
@@ -231,6 +234,8 @@ struct ModelData {
|
|||||||
mesh_plans: BTreeMap<String, MeshPlan>,
|
mesh_plans: BTreeMap<String, MeshPlan>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
face_merges: Vec<FaceMerge>,
|
face_merges: Vec<FaceMerge>,
|
||||||
|
#[serde(default)]
|
||||||
|
stitches: Vec<Stitch>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<ModelData> for Model {
|
impl TryFrom<ModelData> for Model {
|
||||||
@@ -323,6 +328,9 @@ impl TryFrom<ModelData> for Model {
|
|||||||
for v in d.face_merges {
|
for v in d.face_merges {
|
||||||
m.add_face_merge(v)?;
|
m.add_face_merge(v)?;
|
||||||
}
|
}
|
||||||
|
for v in d.stitches {
|
||||||
|
m.add_stitch(v)?;
|
||||||
|
}
|
||||||
Ok(m)
|
Ok(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -456,6 +464,7 @@ impl Model {
|
|||||||
relaxations: self.relaxations.clone(),
|
relaxations: self.relaxations.clone(),
|
||||||
mesh_plans: self.mesh_plans.clone(),
|
mesh_plans: self.mesh_plans.clone(),
|
||||||
face_merges: self.face_merges.clone(),
|
face_merges: self.face_merges.clone(),
|
||||||
|
stitches: self.stitches.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,7 +515,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Counts of each kind of definition, for summaries.
|
/// Counts of each kind of definition, for summaries.
|
||||||
pub fn definition_counts(&self) -> [(&'static str, usize); 17] {
|
pub fn definition_counts(&self) -> [(&'static str, usize); 18] {
|
||||||
[
|
[
|
||||||
("sets", self.sets.len()),
|
("sets", self.sets.len()),
|
||||||
("materials", self.materials.len()),
|
("materials", self.materials.len()),
|
||||||
@@ -525,6 +534,7 @@ impl Model {
|
|||||||
("partitions", self.partitions.len()),
|
("partitions", self.partitions.len()),
|
||||||
("seeds", self.seeds.len()),
|
("seeds", self.seeds.len()),
|
||||||
("face_merges", self.face_merges.len()),
|
("face_merges", self.face_merges.len()),
|
||||||
|
("stitches", self.stitches.len()),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
//! model keeps the merges in the order they were made, because a merge may name the face an
|
//! model keeps the merges in the order they were made, because a merge may name the face an
|
||||||
//! earlier one made (`merge:<name>`); they are applied after the partitions, so a body named may
|
//! earlier one made (`merge:<name>`); they are applied after the partitions, so a body named may
|
||||||
//! be a partition cell.
|
//! be a partition cell.
|
||||||
|
//!
|
||||||
|
//! A **stitch** closes an imported surface's gaps under a tolerance for meshing (the geometry
|
||||||
|
//! layer welds its open points and splits T-junctions).
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -30,6 +33,52 @@ pub struct FaceMerge {
|
|||||||
pub into: Option<String>,
|
pub into: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Most stitches a model holds.
|
||||||
|
pub const MAX_STITCHES: usize = 1024;
|
||||||
|
|
||||||
|
/// An imported surface's gaps under `tolerance` (mm) closed for meshing (`geometry.stitch`).
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Stitch {
|
||||||
|
pub name: String,
|
||||||
|
/// The imported surface, by name.
|
||||||
|
pub surface: String,
|
||||||
|
pub tolerance: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
/// The stitches, in the order they apply.
|
||||||
|
pub fn stitches(&self) -> &[Stitch] {
|
||||||
|
&self.stitches
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a stitch after the others.
|
||||||
|
pub fn add_stitch(&mut self, stitch: Stitch) -> Result<()> {
|
||||||
|
let name = stitch.name.clone();
|
||||||
|
check_name("stitch", &name)?;
|
||||||
|
if self.stitches.iter().any(|s| s.name == name) {
|
||||||
|
return Err(Error::Invalid(format!("stitch `{name}` already exists")));
|
||||||
|
}
|
||||||
|
if self.stitches.len() >= MAX_STITCHES {
|
||||||
|
return Err(Error::Limit(format!("a model holds at most {MAX_STITCHES} stitches")));
|
||||||
|
}
|
||||||
|
let invalid = |m: String| context("stitch", &name, Error::Invalid(m));
|
||||||
|
if stitch.surface.trim().is_empty() || stitch.surface.len() > MAX_GEOMETRY_NAME_LEN {
|
||||||
|
return Err(invalid(format!("the surface name is 1 to {MAX_GEOMETRY_NAME_LEN} bytes")));
|
||||||
|
}
|
||||||
|
if !(stitch.tolerance > 0.0 && stitch.tolerance <= crate::MAX_COORD) {
|
||||||
|
return Err(invalid("the tolerance must be a positive length".into()));
|
||||||
|
}
|
||||||
|
self.stitches.push(stitch);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_stitch(&mut self, name: &str) -> Result<Stitch> {
|
||||||
|
let i = self.stitches.iter().position(|s| s.name == name).ok_or_else(|| Error::NotFound(format!("stitch `{name}`")))?;
|
||||||
|
Ok(self.stitches.remove(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
/// The face merges, in the order they apply.
|
/// The face merges, in the order they apply.
|
||||||
pub fn face_merges(&self) -> &[FaceMerge] {
|
pub fn face_merges(&self) -> &[FaceMerge] {
|
||||||
@@ -83,6 +132,20 @@ mod tests {
|
|||||||
FaceMerge { name: name.into(), body: "Base".into(), faces: faces.iter().map(|f| f.to_string()).collect(), into: None }
|
FaceMerge { name: name.into(), body: "Base".into(), faces: faces.iter().map(|f| f.to_string()).collect(), into: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stitches_are_kept_in_order_and_checked() {
|
||||||
|
let mut m = Model::default();
|
||||||
|
let st = |n: &str, t: f64| Stitch { name: n.into(), surface: "Mesh1".into(), tolerance: t };
|
||||||
|
m.add_stitch(st("Gaps", 0.1)).unwrap();
|
||||||
|
for bad in [st("Gaps", 0.1), st("A", 0.0), st("B", f64::NAN), st("C", -1.0), Stitch { surface: " ".into(), ..st("D", 0.1) }] {
|
||||||
|
assert!(m.add_stitch(bad).is_err());
|
||||||
|
}
|
||||||
|
let back: Model = serde_json::from_str(&serde_json::to_string(&m).unwrap()).unwrap();
|
||||||
|
assert_eq!(back.stitches(), m.stitches());
|
||||||
|
m.remove_stitch("Gaps").unwrap();
|
||||||
|
assert!(m.stitches().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
/// Merges keep their order, are saved and read back, and bad ones are refused.
|
/// Merges keep their order, are saved and read back, and bad ones are refused.
|
||||||
#[test]
|
#[test]
|
||||||
fn merges_are_kept_in_order_and_checked() {
|
fn merges_are_kept_in_order_and_checked() {
|
||||||
|
|||||||
Reference in new issue
Block a user