forked from 1b-agent/fieldcraft-upstream
Model: datum points, drawn as a small cross; each definition's users are public
Datum::Point joins planes and axes (checked like them, refused by partitions with the kind named). The users each remove_* refuses with (sets, materials, boundary conditions, loads, reference points, fluid domains, datums) are public functions, so a caller can list them before deleting. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
c1fd58c6ce
commit
0bf150633c
9 files changed
+104
-30
No files matched your search
@@ -232,10 +232,14 @@ impl Model {
|
||||
replace_existing(&mut self.boundary_conditions, "boundary condition", name, bc)
|
||||
}
|
||||
|
||||
/// The steps that list a boundary condition, as `step \`name\`` strings.
|
||||
pub fn boundary_condition_users(&self, name: &str) -> Vec<String> {
|
||||
self.steps.iter().filter(|s| s.boundary_conditions.contains(name)).map(|s| format!("step `{}`", s.name)).collect()
|
||||
}
|
||||
|
||||
/// Remove a boundary condition that no step lists.
|
||||
pub fn remove_boundary_condition(&mut self, name: &str) -> Result<()> {
|
||||
let users = self.steps.iter().filter(|s| s.boundary_conditions.contains(name)).map(|s| format!("step `{}`", s.name)).collect();
|
||||
not_in_use("boundary condition", name, users)?;
|
||||
not_in_use("boundary condition", name, self.boundary_condition_users(name))?;
|
||||
remove_existing(&mut self.boundary_conditions, "boundary condition", name).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,12 @@ impl Model {
|
||||
}
|
||||
|
||||
pub fn remove_reference_point(&mut self, name: &str) -> Result<()> {
|
||||
not_in_use("reference point", name, self.reference_point_users(name))?;
|
||||
remove_existing(&mut self.reference_points, "reference point", name).map(|_| ())
|
||||
}
|
||||
|
||||
/// The definitions that refer to a reference point, as `kind \`name\`` strings.
|
||||
pub fn reference_point_users(&self, name: &str) -> Vec<String> {
|
||||
let mut users: Vec<String> =
|
||||
self.couplings.iter().filter(|(_, c)| c.reference_point == name).map(|(n, _)| format!("coupling `{n}`")).collect();
|
||||
users.extend(
|
||||
@@ -105,8 +111,7 @@ impl Model {
|
||||
);
|
||||
users.extend(self.loads.iter().filter(|(_, l)| l.reference_point() == Some(name)).map(|(n, _)| format!("load `{n}`")));
|
||||
users.extend(self.discrete_users(|t| t.reference_point() == Some(name)));
|
||||
not_in_use("reference point", name, users)?;
|
||||
remove_existing(&mut self.reference_points, "reference point", name).map(|_| ())
|
||||
users
|
||||
}
|
||||
|
||||
/// The node label of each reference point in decks.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Datum planes and axes, and the partitions that cut the geometry by datum planes.
|
||||
//! Datum planes, axes and points, and the partitions that cut the geometry by datum planes.
|
||||
//!
|
||||
//! A datum is a construction reference in model coordinates: a plane (a point and its unit
|
||||
//! normal) or an axis (a point and its unit direction). Partitions, mirrors and symmetry
|
||||
//! conditions name them. A **partition** cuts bodies of the geometry by a datum plane into cells
|
||||
//! normal), an axis (a point and its unit direction) or a point. Partitions, mirrors and symmetry
|
||||
//! conditions name planes; points are inputs for planes and axes (their coordinates are taken
|
||||
//! when those are made). A **partition** cuts bodies of the geometry by a datum plane into cells
|
||||
//! (the geometry layer does the cutting); the model keeps the partitions in the order they were
|
||||
//! made, because each cuts the cells the ones before it left.
|
||||
|
||||
@@ -19,7 +20,7 @@ pub const MAX_PARTITION_BODIES: usize = 1000;
|
||||
/// Longest body name a partition holds.
|
||||
const MAX_BODY_NAME: usize = 256;
|
||||
|
||||
/// A datum plane or axis.
|
||||
/// A datum plane, axis or point.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Datum {
|
||||
@@ -27,6 +28,8 @@ pub enum Datum {
|
||||
Plane { point: [f64; 3], normal: [f64; 3] },
|
||||
/// A point on the axis and its unit direction.
|
||||
Axis { point: [f64; 3], direction: [f64; 3] },
|
||||
/// A point.
|
||||
Point { point: [f64; 3] },
|
||||
}
|
||||
|
||||
impl Datum {
|
||||
@@ -48,14 +51,40 @@ impl Datum {
|
||||
Ok(match self {
|
||||
Datum::Plane { point: p, normal } => Datum::Plane { point: point(p)?, normal: unit("normal", normal)? },
|
||||
Datum::Axis { point: p, direction } => Datum::Axis { point: point(p)?, direction: unit("direction", direction)? },
|
||||
Datum::Point { point: p } => Datum::Point { point: point(p)? },
|
||||
})
|
||||
}
|
||||
|
||||
/// The plane's point and unit normal; `None` for an axis.
|
||||
/// The plane's point and unit normal; `None` for an axis or a point.
|
||||
pub fn plane(&self) -> Option<([f64; 3], [f64; 3])> {
|
||||
match *self {
|
||||
Datum::Plane { point, normal } => Some((point, normal)),
|
||||
Datum::Axis { .. } => None,
|
||||
Datum::Axis { .. } | Datum::Point { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The axis's point and unit direction; `None` for a plane or a point.
|
||||
pub fn axis(&self) -> Option<([f64; 3], [f64; 3])> {
|
||||
match *self {
|
||||
Datum::Axis { point, direction } => Some((point, direction)),
|
||||
Datum::Plane { .. } | Datum::Point { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The point's coordinates; `None` for a plane or an axis.
|
||||
pub fn point(&self) -> Option<[f64; 3]> {
|
||||
match *self {
|
||||
Datum::Point { point } => Some(point),
|
||||
Datum::Plane { .. } | Datum::Axis { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `plane`, `axis` or `point`.
|
||||
pub fn kind(&self) -> &'static str {
|
||||
match self {
|
||||
Datum::Plane { .. } => "plane",
|
||||
Datum::Axis { .. } => "axis",
|
||||
Datum::Point { .. } => "point",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +130,9 @@ impl Model {
|
||||
remove_existing(&mut self.datums, "datum", name).map(|_| ())
|
||||
}
|
||||
|
||||
fn datum_users(&self, name: &str) -> Vec<String> {
|
||||
/// 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()
|
||||
}
|
||||
|
||||
@@ -123,7 +154,9 @@ impl Model {
|
||||
let invalid = |m: String| context("partition", &name, Error::Invalid(m));
|
||||
match self.datums.get(&partition.datum) {
|
||||
None => return Err(context("partition", &name, Error::NotFound(format!("datum `{}`", partition.datum)))),
|
||||
Some(d) if d.plane().is_none() => return Err(invalid(format!("datum `{}` is an axis; a partition cuts by a plane", partition.datum))),
|
||||
Some(d) if d.plane().is_none() => {
|
||||
return Err(invalid(format!("datum `{}` is a datum {}; a partition cuts by a plane", partition.datum, d.kind())));
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
if partition.bodies.len() > MAX_PARTITION_BODIES {
|
||||
@@ -168,6 +201,11 @@ mod tests {
|
||||
m.add_partition(p("Half", "Mid")).unwrap();
|
||||
assert!(m.add_partition(p("Half", "Mid")).is_err());
|
||||
assert!(m.add_partition(p("Bad", "Axis")).is_err());
|
||||
m.add_datum("P", Datum::Point { point: [1.0, 2.0, 3.0] }).unwrap();
|
||||
assert_eq!((m.datum("P").and_then(Datum::point), m.datum("P").map(Datum::kind)), (Some([1.0, 2.0, 3.0]), Some("point")));
|
||||
assert!(m.add_partition(p("Bad", "P")).is_err());
|
||||
assert!(m.add_datum("Q", Datum::Point { point: [f64::NAN, 0.0, 0.0] }).is_err());
|
||||
assert_eq!(m.datum_users("Mid"), ["partition `Half`"]);
|
||||
assert!(m.add_partition(p("Bad", "Nope")).is_err());
|
||||
assert!(m.add_partition(Partition { bodies: vec!["A".into(), "A".into()], ..p("Bad", "Mid") }).is_err());
|
||||
assert!(matches!(m.remove_datum("Mid"), Err(Error::InUse(_))));
|
||||
|
||||
@@ -423,9 +423,13 @@ impl Model {
|
||||
replace_existing(&mut self.fluid_domains, "fluid domain", name, domain)
|
||||
}
|
||||
|
||||
/// The steps that solve a fluid domain, as `step \`name\`` strings.
|
||||
pub fn fluid_domain_users(&self, name: &str) -> Vec<String> {
|
||||
self.steps.iter().filter(|s| s.procedure.fluid_domain() == Some(name)).map(|s| format!("step `{}`", s.name)).collect()
|
||||
}
|
||||
|
||||
pub fn remove_fluid_domain(&mut self, name: &str) -> Result<()> {
|
||||
let users = self.steps.iter().filter(|s| s.procedure.fluid_domain() == Some(name)).map(|s| format!("step `{}`", s.name)).collect();
|
||||
not_in_use("fluid domain", name, users)?;
|
||||
not_in_use("fluid domain", name, self.fluid_domain_users(name))?;
|
||||
remove_existing(&mut self.fluid_domains, "fluid domain", name).map(|_| ())
|
||||
}
|
||||
|
||||
@@ -437,7 +441,7 @@ impl Model {
|
||||
}
|
||||
|
||||
/// The fluid domains that use a material.
|
||||
pub(crate) fn fluid_domain_users(&self, material: &str) -> impl Iterator<Item = &str> {
|
||||
pub(crate) fn fluid_domains_of_material(&self, material: &str) -> impl Iterator<Item = &str> {
|
||||
self.fluid_domains.iter().filter(move |(_, d)| d.material == material).map(|(n, _)| n.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,10 +222,14 @@ impl Model {
|
||||
replace_existing(&mut self.loads, "load", name, load)
|
||||
}
|
||||
|
||||
/// The steps that list a load, as `step \`name\`` strings.
|
||||
pub fn load_users(&self, name: &str) -> Vec<String> {
|
||||
self.steps.iter().filter(|s| s.loads.contains(name)).map(|s| format!("step `{}`", s.name)).collect()
|
||||
}
|
||||
|
||||
/// Remove a load that no step lists.
|
||||
pub fn remove_load(&mut self, name: &str) -> Result<()> {
|
||||
let users = self.steps.iter().filter(|s| s.loads.contains(name)).map(|s| format!("step `{}`", s.name)).collect();
|
||||
not_in_use("load", name, users)?;
|
||||
not_in_use("load", name, self.load_users(name))?;
|
||||
remove_existing(&mut self.loads, "load", name).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,17 +511,22 @@ impl Model {
|
||||
pub fn replace_material(&mut self, name: &str, material: Material) -> Result<()> {
|
||||
material.validate().map_err(|e| context("material", name, e))?;
|
||||
if material.fluid.is_none()
|
||||
&& let Some(d) = self.fluid_domain_users(name).next()
|
||||
&& let Some(d) = self.fluid_domains_of_material(name).next()
|
||||
{
|
||||
return Err(Error::Invalid(format!("material `{name}`: fluid domain `{d}` needs its fluid behaviour")));
|
||||
}
|
||||
replace_existing(&mut self.materials, "material", name, material)
|
||||
}
|
||||
|
||||
pub fn remove_material(&mut self, name: &str) -> Result<()> {
|
||||
/// The definitions that refer to a material, as `kind \`name\`` strings.
|
||||
pub fn material_users(&self, name: &str) -> Vec<String> {
|
||||
let mut users: Vec<String> = self.sections.iter().filter(|(_, s)| s.material == name).map(|(n, _)| format!("section `{n}`")).collect();
|
||||
users.extend(self.fluid_domain_users(name).map(|n| format!("fluid domain `{n}`")));
|
||||
not_in_use("material", name, users)?;
|
||||
users.extend(self.fluid_domains_of_material(name).map(|n| format!("fluid domain `{n}`")));
|
||||
users
|
||||
}
|
||||
|
||||
pub fn remove_material(&mut self, name: &str) -> Result<()> {
|
||||
not_in_use("material", name, self.material_users(name))?;
|
||||
remove_existing(&mut self.materials, "material", name).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ impl Model {
|
||||
}
|
||||
|
||||
/// The definitions that refer to a set, as `kind \`name\`` strings.
|
||||
pub(crate) fn set_users(&self, set: &str) -> Vec<String> {
|
||||
pub fn set_users(&self, set: &str) -> Vec<String> {
|
||||
let mut users = Vec::new();
|
||||
users.extend(self.sections.iter().filter(|(_, s)| s.set == set).map(|(n, _)| format!("section `{n}`")));
|
||||
users.extend(self.couplings.iter().filter(|(_, c)| c.set == set).map(|(n, _)| format!("coupling `{n}`")));
|
||||
|
||||
@@ -841,11 +841,14 @@ impl Glyphs {
|
||||
/// How far a datum plane's square reaches from the model's centre, and a datum axis's line,
|
||||
/// as a fraction of the bounding box diagonal.
|
||||
const DATUM_REACH: f64 = 0.6;
|
||||
/// A datum point's cross: each arm's length, as a fraction of the bounding box diagonal.
|
||||
const DATUM_POINT: f64 = 0.02;
|
||||
/// Hatch lines across a datum plane's square.
|
||||
const DATUM_HATCH: usize = 4;
|
||||
|
||||
/// Draw the model's datum planes (an outlined, hatched square around the model, `bounds`) and
|
||||
/// axes (a line through the model), each with its name; returns the box around them.
|
||||
/// Draw the model's datum planes (an outlined, hatched square around the model, `bounds`), axes
|
||||
/// (a line through the model) and points (a small cross), each with its name; returns the box
|
||||
/// around them.
|
||||
pub fn datums(model: &Model, bounds: &Aabb3, colors: &GlyphColors, scene: &mut Scene) -> Aabb3 {
|
||||
let mut d = Draw { scene, bounds: Aabb3::EMPTY, color: colors.datum, width: OUTLINE_WIDTH, on_top: false };
|
||||
let (centre, diagonal) = if bounds.is_empty() { (Vec3::ZERO, 1.0) } else { (bounds.center(), bounds.diagonal().max(1e-9)) };
|
||||
@@ -879,6 +882,14 @@ pub fn datums(model: &Model, bounds: &Aabb3, colors: &GlyphColors, scene: &mut S
|
||||
d.line(c - a * reach, c + a * reach);
|
||||
d.scene.labels.push(SceneLabel { p: c + a * reach, text: name.clone(), color: colors.datum });
|
||||
}
|
||||
fieldcraft_model::Datum::Point { point } => {
|
||||
let p = Vec3::from(point);
|
||||
let arm = DATUM_POINT * diagonal;
|
||||
for axis in [Vec3::X, Vec3::Y, Vec3::Z] {
|
||||
d.line(p - axis * arm, p + axis * arm);
|
||||
}
|
||||
d.scene.labels.push(SceneLabel { p: p + (Vec3::X + Vec3::Y) * arm, text: name.clone(), color: colors.datum });
|
||||
}
|
||||
}
|
||||
}
|
||||
d.bounds
|
||||
@@ -1494,18 +1505,22 @@ mod tests {
|
||||
let mut m = Model::default();
|
||||
m.add_datum("Mid", fieldcraft_model::Datum::Plane { point: [5.0, 0.0, 0.0], normal: [1.0, 0.0, 0.0] }).unwrap();
|
||||
m.add_datum("Ax", fieldcraft_model::Datum::Axis { point: [0.0; 3], direction: [0.0, 0.0, 1.0] }).unwrap();
|
||||
m.add_datum("Pt", fieldcraft_model::Datum::Point { point: [1.0, 2.0, 3.0] }).unwrap();
|
||||
let mut bounds = Aabb3::EMPTY;
|
||||
bounds.add(Vec3::ZERO);
|
||||
bounds.add(Vec3::new(10.0, 10.0, 10.0));
|
||||
let mut scene = Scene::default();
|
||||
let drawn = datums(&m, &bounds, &COLORS, &mut scene);
|
||||
// A square with its hatch (4 + 2 × 3 lines) and the axis line.
|
||||
assert_eq!(scene.lines.len(), 11);
|
||||
// A square with its hatch (4 + 2 × 3 lines), the axis line and the point's three arms,
|
||||
// each crossing at the point.
|
||||
assert_eq!(scene.lines.len(), 14);
|
||||
let p = Vec3::new(1.0, 2.0, 3.0);
|
||||
assert!(scene.lines.iter().rev().take(3).all(|l| ((l.a + l.b) * 0.5).dist(p) < 1e-12));
|
||||
let mut names: Vec<&str> = scene.labels.iter().map(|l| l.text.as_str()).collect();
|
||||
names.sort_unstable();
|
||||
assert_eq!(names, ["Ax", "Mid"]);
|
||||
assert_eq!(names, ["Ax", "Mid", "Pt"]);
|
||||
// The plane's lines lie on x = 5, centred on the model.
|
||||
assert!(scene.lines.iter().skip(1).all(|l| (l.a.x - 5.0).abs() < 1e-12 && (l.b.x - 5.0).abs() < 1e-12));
|
||||
assert!(scene.lines.iter().skip(1).take(10).all(|l| (l.a.x - 5.0).abs() < 1e-12 && (l.b.x - 5.0).abs() < 1e-12));
|
||||
assert!(drawn.diagonal() > bounds.diagonal());
|
||||
}
|
||||
}
|
||||
@@ -658,8 +658,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
|
||||
);
|
||||
}
|
||||
// Datum planes and axes, then the partitions in the order they cut.
|
||||
let datums: Vec<(String, &'static str)> =
|
||||
app.session.model.datums().iter().map(|(n, d)| (n.clone(), if d.plane().is_some() { "plane" } else { "axis" })).collect();
|
||||
let datums: Vec<(String, &'static str)> = app.session.model.datums().iter().map(|(n, d)| (n.clone(), d.kind())).collect();
|
||||
for (i, (n, kind)) in datums.iter().enumerate() {
|
||||
draw_row(
|
||||
app,
|
||||
|
||||
Reference in new issue
Block a user