diff --git a/crates/engine/src/view.rs b/crates/engine/src/view.rs index 50a2442..823b51d 100644 --- a/crates/engine/src/view.rs +++ b/crates/engine/src/view.rs @@ -72,6 +72,7 @@ impl Palette { interaction: Rgb(0x9a, 0x30, 0xc0), thermal: Rgb(0xe8, 0x4c, 0x00), label: Rgb(0x26, 0x2a, 0x32), + datum: Rgb(0x1e, 0x8c, 0x6e), }, }; @@ -156,6 +157,7 @@ pub fn scene(s: &Session, p: &Palette) -> (Scene, Aabb3, Shown) { let mut bounds = base.bounds; let mut scene = model_scene(&base, p); bounds.union(glyphs.draw(|label| base.node_position(label), GlyphShow::ALL, &p.glyphs, &mut scene)); + bounds.union(fieldcraft_render::glyphs::datums(&s.model, &base.bounds, &p.glyphs, &mut scene)); return (scene, bounds, shown); }; shown.step = Some(view.step.clone()); diff --git a/crates/render/src/glyphs.rs b/crates/render/src/glyphs.rs index 1992ddf..c4d9dbe 100644 --- a/crates/render/src/glyphs.rs +++ b/crates/render/src/glyphs.rs @@ -72,6 +72,8 @@ pub struct GlyphColors { pub thermal: Rgb, /// Reference point names. pub label: Rgb, + /// Datum planes and axes. + pub datum: Rgb, } impl GlyphColors { @@ -89,6 +91,7 @@ impl GlyphColors { interaction: mix(self.interaction), thermal: mix(self.thermal), label: mix(self.label), + datum: mix(self.datum), } } @@ -647,6 +650,49 @@ 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; +/// 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. +pub fn datums(model: &Model, bounds: &Aabb3, colors: &GlyphColors, scene: &mut Scene) -> Aabb3 { + let mut d = Draw { scene, bounds: Aabb3::EMPTY }; + let (centre, diagonal) = if bounds.is_empty() { (Vec3::ZERO, 1.0) } else { (bounds.center(), bounds.diagonal().max(1e-9)) }; + let reach = DATUM_REACH * diagonal; + for (name, datum) in model.datums() { + match *datum { + fieldcraft_model::Datum::Plane { point, normal } => { + let (p, n) = (Vec3::from(point), Vec3::from(normal)); + // The model's centre moved onto the plane. + let c = centre - n * (centre - p).dot(n); + let (u, v) = perpendiculars(n); + let corner = |a: f64, b: f64| c + u * (a * reach) + v * (b * reach); + let ring = [corner(-1.0, -1.0), corner(1.0, -1.0), corner(1.0, 1.0), corner(-1.0, 1.0)]; + for (k, a) in ring.iter().enumerate() { + let b = ring.get((k + 1) % 4).copied().unwrap_or(*a); + d.line(*a, b, colors.datum, OUTLINE_WIDTH); + } + for k in 1..DATUM_HATCH { + let t = -1.0 + 2.0 * k as f64 / DATUM_HATCH as f64; + d.line(corner(t, -1.0), corner(t, 1.0), colors.datum, COUPLING_WIDTH); + d.line(corner(-1.0, t), corner(1.0, t), colors.datum, COUPLING_WIDTH); + } + d.scene.labels.push(SceneLabel { p: corner(1.0, 1.0), text: name.clone(), color: colors.datum }); + } + fieldcraft_model::Datum::Axis { point, direction } => { + let (p, a) = (Vec3::from(point), Vec3::from(direction)); + let c = p + a * (centre - p).dot(a); + d.line(c - a * reach, c + a * reach, colors.datum, SHAFT_WIDTH); + d.scene.labels.push(SceneLabel { p: c + a * reach, text: name.clone(), color: colors.datum }); + } + } + } + d.bounds +} + /// Symbol primitives into a scene. struct Draw<'s> { scene: &'s mut Scene, @@ -745,8 +791,14 @@ mod tests { use super::*; - const COLORS: GlyphColors = - GlyphColors { constraint: Rgb(0, 0, 255), load: Rgb(255, 0, 0), interaction: Rgb(0, 255, 0), thermal: Rgb(255, 128, 0), label: Rgb(0, 0, 0) }; + const COLORS: GlyphColors = GlyphColors { + constraint: Rgb(0, 0, 255), + load: Rgb(255, 0, 0), + interaction: Rgb(0, 255, 0), + thermal: Rgb(255, 128, 0), + label: Rgb(0, 0, 0), + datum: Rgb(0, 128, 128), + }; /// A row of `n` unit hex8 cells along x. fn bar(n: u32) -> Model { @@ -948,4 +1000,24 @@ mod tests { assert!(thin::(&[], 1.0, 10).is_empty()); assert!(thin(&[(1u32, Vec3::new(f64::INFINITY, 0.0, 0.0))], 1.0, 10).is_empty()); } + + #[test] + fn datums_draw_around_the_model_with_their_names() { + 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(); + 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); + let mut names: Vec<&str> = scene.labels.iter().map(|l| l.text.as_str()).collect(); + names.sort_unstable(); + assert_eq!(names, ["Ax", "Mid"]); + // 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!(drawn.diagonal() > bounds.diagonal()); + } } diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index b680339..1383a5e 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -1,4 +1,5 @@ -//! The browser (left): the model as a tree. Geometry and Mesh with eye toggles, then the model's +//! The browser (left): the model as a tree. Geometry (the design's bodies, datums and +//! partitions) and Mesh with eye toggles, then the model's //! definitions (sets, materials, sections, steps, loads and boundary conditions, interactions) //! with counts (Loads & BCs and Interactions with eye toggles for their viewport symbols), and //! the solved steps' results. Clicking a definition selects it (its properties show on the @@ -388,6 +389,26 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec) { &Row { depth: 2, icon: Some("geometry.import.mesh"), label: s, ..Default::default() }, ); } + // 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(); + for (i, (n, kind)) in datums.iter().enumerate() { + draw_row( + app, + ui, + egui::Id::new(("fc_row_datum", i)), + &Row { depth: 2, icon: Some("geometry.datum"), label: n, note: Some((*kind).into()), ..Default::default() }, + ); + } + let partitions: Vec<(String, String)> = app.session.model.partitions().iter().map(|p| (p.name.clone(), p.datum.clone())).collect(); + for (i, (n, datum)) in partitions.iter().enumerate() { + draw_row( + app, + ui, + egui::Id::new(("fc_row_partition", i)), + &Row { depth: 2, icon: Some("geometry.partition"), label: n, note: Some(format!("by {datum}")), ..Default::default() }, + ); + } if design.is_none() && surfaces.is_empty() { draw_row( app, diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index a410347..6ae3dd9 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -750,7 +750,10 @@ impl ModelApp { }; // Symbols follow the drawn nodes: the deformed shape of a results plot. let drawn = self.plot.1.as_ref().map_or(&self.display.1, |p| &p.display); - let symbols = scene::symbols(&mut s, &self.glyphs, |n| drawn.node_position(n), show, self.plot.1.is_some(), &t); + let mut symbols = scene::symbols(&mut s, &self.glyphs, |n| drawn.node_position(n), show, self.plot.1.is_some(), &t); + if show.geometry && self.plot.1.is_none() { + symbols.union(scene::datums(&mut s, &self.session.model, &self.display.1.bounds, &t)); + } self.scene = (Some(key), s, symbols); } let mut bounds = self.display.1.bounds; diff --git a/crates/ui-egui/src/scene.rs b/crates/ui-egui/src/scene.rs index d749944..20056d8 100644 --- a/crates/ui-egui/src/scene.rs +++ b/crates/ui-egui/src/scene.rs @@ -69,6 +69,11 @@ pub fn symbols(s: &mut Scene, glyphs: &Glyphs, pos: impl Fn(u32) -> Option glyphs.draw(pos, GlyphShow { loads: show.loads, interactions: show.interactions }, &colors, s) } +/// Draw the model's datum planes and axes around `bounds` into `s`; returns the box around them. +pub fn datums(s: &mut Scene, model: &fieldcraft_model::Model, bounds: &Aabb3, t: &Tokens) -> Aabb3 { + fieldcraft_render::glyphs::datums(model, bounds, &t.glyph_colors(), s) +} + /// The scene for `display` drawn in `style`. pub fn build(display: &DisplayMesh, style: Style, show: Show, t: &Tokens) -> Scene { let mut s = Scene { background: Some((rgb(t.viewport_top), rgb(t.viewport_bottom))), ..Default::default() }; diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index ca6df42..2e288cf 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -82,6 +82,8 @@ pub struct Tokens { pub glyph_interaction: Color32, /// Temperatures, heat fluxes, convection and heat sources. pub glyph_thermal: Color32, + /// Datum planes and axes. + pub glyph_datum: Color32, // ---- selection ---- /// What is selected in the viewport, and what a click would pick under the pointer. pub pick_selected: Color32, @@ -159,6 +161,7 @@ impl Tokens { glyph_load: Color32::from_rgb(0xd0, 0x24, 0x24), glyph_interaction: Color32::from_rgb(0x9a, 0x30, 0xc0), glyph_thermal: Color32::from_rgb(0xe8, 0x4c, 0x00), + glyph_datum: Color32::from_rgb(0x1e, 0x8c, 0x6e), pick_selected: Color32::from_rgb(0xd6, 0x00, 0x7f), pick_hover: Color32::from_rgb(0xf0, 0x7c, 0xc0), } @@ -221,6 +224,7 @@ impl Tokens { t.glyph_load = Color32::from_rgb(0xff, 0x5a, 0x4c); t.glyph_interaction = Color32::from_rgb(0xd2, 0x82, 0xff); t.glyph_thermal = Color32::from_rgb(0xff, 0x8c, 0x00); + t.glyph_datum = Color32::from_rgb(0x5a, 0xd8, 0xaa); t.pick_selected = Color32::from_rgb(0xff, 0x3f, 0xb4); t.pick_hover = Color32::from_rgb(0xff, 0xa6, 0xdc); t @@ -239,6 +243,7 @@ impl Tokens { interaction: rgb(self.glyph_interaction), thermal: rgb(self.glyph_thermal), label: rgb(self.overlay_text), + datum: rgb(self.glyph_datum), } }