diff --git a/crates/model/src/bcs.rs b/crates/model/src/bcs.rs index e4babb4..53b8291 100644 --- a/crates/model/src/bcs.rs +++ b/crates/model/src/bcs.rs @@ -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 { + 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(|_| ()) } } diff --git a/crates/model/src/constraints.rs b/crates/model/src/constraints.rs index e0bda58..9cd9f85 100644 --- a/crates/model/src/constraints.rs +++ b/crates/model/src/constraints.rs @@ -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 { let mut users: Vec = 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. diff --git a/crates/model/src/datums.rs b/crates/model/src/datums.rs index efdfcc7..e17a819 100644 --- a/crates/model/src/datums.rs +++ b/crates/model/src/datums.rs @@ -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 { + /// 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() } @@ -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(_)))); diff --git a/crates/model/src/fluids.rs b/crates/model/src/fluids.rs index 7932241..a580d93 100644 --- a/crates/model/src/fluids.rs +++ b/crates/model/src/fluids.rs @@ -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 { + 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 { + pub(crate) fn fluid_domains_of_material(&self, material: &str) -> impl Iterator { self.fluid_domains.iter().filter(move |(_, d)| d.material == material).map(|(n, _)| n.as_str()) } } diff --git a/crates/model/src/loads.rs b/crates/model/src/loads.rs index 24dfedd..eb48c9b 100644 --- a/crates/model/src/loads.rs +++ b/crates/model/src/loads.rs @@ -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 { + 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(|_| ()) } } diff --git a/crates/model/src/materials.rs b/crates/model/src/materials.rs index 2cdc840..ae3b4fc 100644 --- a/crates/model/src/materials.rs +++ b/crates/model/src/materials.rs @@ -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 { let mut users: Vec = 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(|_| ()) } } diff --git a/crates/model/src/sets.rs b/crates/model/src/sets.rs index 08c9cab..b0d4bbc 100644 --- a/crates/model/src/sets.rs +++ b/crates/model/src/sets.rs @@ -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 { + pub fn set_users(&self, set: &str) -> Vec { 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}`"))); diff --git a/crates/render/src/glyphs.rs b/crates/render/src/glyphs.rs index 4a7efbb..03ce18e 100644 --- a/crates/render/src/glyphs.rs +++ b/crates/render/src/glyphs.rs @@ -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()); } } diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index 19fce71..697e40c 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -658,8 +658,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec) { ); } // 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,