forked from 1b-agent/fieldcraft-upstream
model: face merges, kept in the order they apply (#505)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
b126c2293b
commit
22d709e604
2 files changed
+111
-1
No files matched your search
+13
-1
@@ -53,6 +53,7 @@ pub mod sets;
|
||||
pub mod steps;
|
||||
pub mod tie;
|
||||
pub mod units;
|
||||
pub mod virtual_faces;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
@@ -84,6 +85,7 @@ pub use steps::{
|
||||
};
|
||||
pub use tie::Tie;
|
||||
pub use units::UnitSystem;
|
||||
pub use virtual_faces::FaceMerge;
|
||||
|
||||
/// 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.
|
||||
@@ -172,6 +174,9 @@ pub struct Model {
|
||||
/// Mesh plans (`mesh.plan`), by name.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
mesh_plans: BTreeMap<String, MeshPlan>,
|
||||
/// Face merges (virtual topology), in the order they apply.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
face_merges: Vec<FaceMerge>,
|
||||
}
|
||||
|
||||
/// A [`Model`] as stored, before it is checked.
|
||||
@@ -224,6 +229,8 @@ struct ModelData {
|
||||
relaxations: BTreeMap<String, Relaxation>,
|
||||
#[serde(default)]
|
||||
mesh_plans: BTreeMap<String, MeshPlan>,
|
||||
#[serde(default)]
|
||||
face_merges: Vec<FaceMerge>,
|
||||
}
|
||||
|
||||
impl TryFrom<ModelData> for Model {
|
||||
@@ -313,6 +320,9 @@ impl TryFrom<ModelData> for Model {
|
||||
for (n, v) in d.mesh_plans {
|
||||
m.set_mesh_plan(&n, v)?;
|
||||
}
|
||||
for v in d.face_merges {
|
||||
m.add_face_merge(v)?;
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
}
|
||||
@@ -445,6 +455,7 @@ impl Model {
|
||||
seeds: self.seeds.clone(),
|
||||
relaxations: self.relaxations.clone(),
|
||||
mesh_plans: self.mesh_plans.clone(),
|
||||
face_merges: self.face_merges.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,7 +506,7 @@ impl Model {
|
||||
}
|
||||
|
||||
/// Counts of each kind of definition, for summaries.
|
||||
pub fn definition_counts(&self) -> [(&'static str, usize); 16] {
|
||||
pub fn definition_counts(&self) -> [(&'static str, usize); 17] {
|
||||
[
|
||||
("sets", self.sets.len()),
|
||||
("materials", self.materials.len()),
|
||||
@@ -513,6 +524,7 @@ impl Model {
|
||||
("datums", self.datums.len()),
|
||||
("partitions", self.partitions.len()),
|
||||
("seeds", self.seeds.len()),
|
||||
("face_merges", self.face_merges.len()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Virtual topology: faces of a body merged into one face for meshing only.
|
||||
//!
|
||||
//! A **face merge** names adjacent faces of a body, by persistent name, that the meshers take as
|
||||
//! one face (the geometry layer remaps the tessellation they mesh; the B-rep is unchanged). The
|
||||
//! 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
|
||||
//! be a partition cell.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::geometry::MAX_GEOMETRY_NAME_LEN;
|
||||
use crate::{Error, Model, Result, check_name, context};
|
||||
|
||||
/// Most face merges a model holds.
|
||||
pub const MAX_FACE_MERGES: usize = 1024;
|
||||
/// Most faces one merge names.
|
||||
pub const MAX_FACE_MERGE_FACES: usize = 1024;
|
||||
|
||||
/// Faces of one body meshed as one face, `merge:<name>`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FaceMerge {
|
||||
pub name: String,
|
||||
pub body: String,
|
||||
/// Two or more faces by persistent name (a name also stands for the pieces a partition cut
|
||||
/// it into).
|
||||
pub faces: Vec<String>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// The face merges, in the order they apply.
|
||||
pub fn face_merges(&self) -> &[FaceMerge] {
|
||||
&self.face_merges
|
||||
}
|
||||
|
||||
/// Add a face merge after the others.
|
||||
pub fn add_face_merge(&mut self, merge: FaceMerge) -> Result<()> {
|
||||
let name = merge.name.clone();
|
||||
check_name("face merge", &name)?;
|
||||
if self.face_merges.iter().any(|m| m.name == name) {
|
||||
return Err(Error::Invalid(format!("face merge `{name}` already exists")));
|
||||
}
|
||||
if self.face_merges.len() >= MAX_FACE_MERGES {
|
||||
return Err(Error::Limit(format!("a model holds at most {MAX_FACE_MERGES} face merges")));
|
||||
}
|
||||
let invalid = |m: String| context("face merge", &name, Error::Invalid(m));
|
||||
let named = |s: &str| !s.trim().is_empty() && s.len() <= MAX_GEOMETRY_NAME_LEN;
|
||||
if !named(&merge.body) {
|
||||
return Err(invalid(format!("the body name is 1 to {MAX_GEOMETRY_NAME_LEN} bytes")));
|
||||
}
|
||||
if !(2..=MAX_FACE_MERGE_FACES).contains(&merge.faces.len()) {
|
||||
return Err(invalid(format!("a merge names 2 to {MAX_FACE_MERGE_FACES} faces")));
|
||||
}
|
||||
for (i, f) in merge.faces.iter().enumerate() {
|
||||
if !named(f) {
|
||||
return Err(invalid(format!("face names are 1 to {MAX_GEOMETRY_NAME_LEN} bytes")));
|
||||
}
|
||||
if merge.faces.get(..i).is_some_and(|before| before.contains(f)) {
|
||||
return Err(invalid(format!("face `{f}` is listed twice")));
|
||||
}
|
||||
}
|
||||
self.face_merges.push(merge);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_face_merge(&mut self, name: &str) -> Result<FaceMerge> {
|
||||
let i = self.face_merges.iter().position(|m| m.name == name).ok_or_else(|| Error::NotFound(format!("face merge `{name}`")))?;
|
||||
Ok(self.face_merges.remove(i))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn merge(name: &str, faces: &[&str]) -> FaceMerge {
|
||||
FaceMerge { name: name.into(), body: "Base".into(), faces: faces.iter().map(|f| f.to_string()).collect() }
|
||||
}
|
||||
|
||||
/// Merges keep their order, are saved and read back, and bad ones are refused.
|
||||
#[test]
|
||||
fn merges_are_kept_in_order_and_checked() {
|
||||
let mut m = Model::default();
|
||||
m.add_face_merge(merge("Fillet", &["F3:a", "F3:b"])).unwrap();
|
||||
m.add_face_merge(merge("More", &["merge:Fillet", "F1:+z"])).unwrap();
|
||||
assert!(m.add_face_merge(merge("Fillet", &["x", "y"])).is_err(), "taken");
|
||||
assert!(m.add_face_merge(merge("One", &["x"])).is_err(), "one face");
|
||||
assert!(m.add_face_merge(merge("Twice", &["x", "x"])).is_err());
|
||||
assert!(m.add_face_merge(merge("Blank", &["x", " "])).is_err());
|
||||
assert!(m.add_face_merge(merge("9bad", &["x", "y"])).is_err());
|
||||
assert!(m.add_face_merge(FaceMerge { body: String::new(), ..merge("NoBody", &["x", "y"]) }).is_err());
|
||||
let back: Model = serde_json::from_str(&serde_json::to_string(&m).unwrap()).unwrap();
|
||||
assert_eq!(back.face_merges(), m.face_merges());
|
||||
assert_eq!(m.remove_face_merge("Fillet").unwrap().name, "Fillet");
|
||||
assert!(m.remove_face_merge("Fillet").is_err());
|
||||
assert_eq!(m.face_merges().len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user