From a09e3c856b92512659d0588dd479c792e57913e7 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:19:54 -0400 Subject: [PATCH] ui: pick vertices, points and cells from the viewport - Select: Vertices picks the design's B-rep vertices (meshed or not), and a selected vertex is marked (#346). - Use selection on Datum Point (vertex, edge, face, or a hole's centre) and Reference Point (geometry.centre: a hole's centre, a vertex, nodes) (#346, #351). - Partitioned Mesh: Use selection takes the picked bodies as cells; the cell list has All, None and Invert; hovering a cell's row (or its method row) highlights the cell in the viewport (#344). - Rigid Wall's form takes its set from the selection; the Interactions branch lists the explicit steps' rigid walls, each opening its step (#350). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo --- crates/ui-egui/src/browser.rs | 26 ++++++++++- crates/ui-egui/src/context.rs | 22 +++++++++ crates/ui-egui/src/lib.rs | 21 +++++++++ crates/ui-egui/src/scene.rs | 2 +- crates/ui-egui/src/selection.rs | 80 ++++++++++++++++++++++++++++++--- 5 files changed, 143 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index 3ec89e1..41fc73e 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -771,6 +771,21 @@ pub fn browser(app: &mut ModelApp, ui: &mut egui::Ui) { } /// The viewport setting a definition group's eye shows and hides: the setup's symbols. +/// The rigid walls of the explicit steps: a row label (`Rigid wall on front (Impact)`) and the +/// step each belongs to. +fn rigid_walls(m: &Model) -> Vec<(String, String)> { + m.steps() + .iter() + .filter_map(|s| match &s.procedure { + fieldcraft_model::Procedure::ExplicitDynamic(e) => { + Some(e.rigid_walls.iter().map(move |w| (format!("Rigid wall on {} ({})", w.set, s.name), s.name.clone()))) + } + _ => None, + }) + .flatten() + .collect() +} + fn eye_flag<'u>(state: &'u mut crate::UiState, group: &str) -> Option<&'u mut bool> { match group { "loads" => Some(&mut state.show_loads), @@ -958,6 +973,8 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec) { ]; for (key, label, kinds) in groups { let items: Vec<(Def, String)> = kinds.iter().flat_map(|k| k.names(&app.session.model).into_iter().map(move |n| (*k, n))).collect(); + // Rigid walls belong to their explicit step; they are listed here too, and open it. + let walls: Vec<(String, String)> = if key == "interactions" { rigid_walls(&app.session.model) } else { Vec::new() }; let icon = kinds.first().map(|k| k.icon()); let eye = eye_flag(&mut app.ui, key).map(|v| *v); let (mark, tip) = state(key); @@ -971,8 +988,8 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec) { eye, icon, label, - note: Some(format!("{}", items.len())), - dim: items.is_empty(), + note: Some(format!("{}", items.len() + walls.len())), + dim: items.is_empty() && walls.is_empty(), mark, tip, ..Default::default() @@ -992,6 +1009,11 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec) { let row = Row { depth: 2, icon: Some(icon), moves, label: &name, ..Default::default() }; item_row(app, ui, id, row, Target::Def(kind, name.clone()), acts); } + for (i, (label, step)) in walls.into_iter().enumerate() { + let id = egui::Id::new(("fc_row_rigid_wall", i)); + let row = Row { depth: 2, icon: Some("interaction.rigid_wall"), label: &label, ..Default::default() }; + item_row(app, ui, id, row, Target::Def(Def::Step, step), acts); + } } ghost_rows(app, ui, &stages, key, acts); } diff --git a/crates/ui-egui/src/context.rs b/crates/ui-egui/src/context.rs index b325769..bbe68fc 100644 --- a/crates/ui-egui/src/context.rs +++ b/crates/ui-egui/src/context.rs @@ -26,6 +26,11 @@ pub fn has_context(app: &mut ModelApp) -> bool { } pub fn panel(app: &mut ModelApp, ui: &mut egui::Ui) { + // A form's row hovered last frame no longer is once the form is gone. + if app.dialog.is_none() && app.pick.form_hover { + app.pick.form_hover = false; + app.set_hover(None); + } if !has_context(app) { return; } @@ -150,6 +155,9 @@ fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) { "Names a set of the selection here: an existing set with the same members, or a new one made on OK (model.set.create)" } selection::Target::Edges => "The selected design edges (Select: Edges) become the edges seeded", + selection::Target::Position => "Puts the point at what is selected: a vertex or node, the centre of a hole (its faces or rim), or the centroid of faces and edges", + selection::Target::DatumPoint => "Puts the point on what is selected: a vertex, an edge's midpoint, a face's centre, or the centre of a hole or of several items", + selection::Target::Bodies => "The selected bodies (Select: Bodies), or the bodies of the selected faces, become the cells", }; use_selection = ui.add_enabled(selected.is_some(), egui::Button::new("Use selection")).on_hover_text(tip).clicked(); match &selected { @@ -169,6 +177,7 @@ fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) { }); let mut invalid = 0; let mut create = None; + let mut hover_body = None; // What is missing or wrong, by the fields' labels: OK waits for it (in either view). let problems = match (spec.form, params.as_object()) { (Some(fields), Some(obj)) => crate::form::messages(fields, obj), @@ -211,6 +220,7 @@ fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) { let out = crate::form::show(ui, t, &cx, fields, &mut obj); invalid = out.invalid; create = out.create; + hover_body = out.hover_body; if out.changed { d.text = Value::Object(obj).to_string(); d.error = None; @@ -258,6 +268,18 @@ fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) { if ui.input(|i| i.key_pressed(egui::Key::Escape)) && app.ui.menu.is_none() && !app.ui.palette { cancel = true; } + // A body's row under the pointer is highlighted in the viewport, as a hover there is. + match hover_body.and_then(|b| fieldcraft_model::GeometryRef::body(b).ok()) { + Some(g) => { + app.pick.form_hover = true; + app.set_hover(Some(selection::Item::Geometry(g))); + } + None if app.pick.form_hover => { + app.pick.form_hover = false; + app.set_hover(None); + } + None => {} + } if cancel { app.close_dialog(); } else if let Some((collection, path)) = create { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index cfa3bcb..dc06d89 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -237,6 +237,8 @@ pub struct PickState { pub dragging: Option<(egui::Pos2, egui::Pos2)>, /// Where the pointer was when the hover was worked out (window points). pub hover_at: Option, + /// The hover is a body whose row in a command form is under the pointer, not the viewport's. + pub form_hover: bool, /// Bumped whenever the selection changes, and whenever the hover does. generation: u64, hover_generation: u64, @@ -957,6 +959,7 @@ impl ModelApp { name: body.name.clone(), faces: body.faces().iter().map(|f| f.name.clone()).collect(), edges: body.edge_names(&mesh), + vertices: body.vertex_names(&mesh).into_iter().map(|v| (v.name, v.point.into())).collect(), }; Some((names, mesh)) }) @@ -980,6 +983,7 @@ impl ModelApp { name: s.name.clone(), faces: (0..s.faces).map(|f| f.to_string()).collect(), edges: Vec::new(), + vertices: Vec::new(), }); } let was_empty = self.display.1.bounds.is_empty(); @@ -1423,6 +1427,23 @@ impl ModelApp { selection::Target::Edges => { selection::with_edges(¶ms, &self.pick.selection).ok_or("no edge is selected: pick edges with Select: Edges")? } + selection::Target::Position | selection::Target::DatumPoint => { + // A query that fills the form: it changes nothing and has no reply panel of its own. + let centre = self.session.execute("geometry.centre", &self.pick.selection.params()).map_err(|e| e.to_string())?; + let mut out = params.as_object().cloned().unwrap_or_default(); + if target == selection::Target::Position { + out.insert("xyz".into(), centre.get("point").cloned().unwrap_or(Value::Null)); + } else { + out.insert("by".into(), selection::datum_point_by(&self.pick.selection, ¢re)); + } + Value::Object(out) + } + selection::Target::Bodies => { + let bodies = selection::bodies(&self.pick.selection).ok_or("no geometry is selected: pick cells with Select: Bodies")?; + let mut out = params.as_object().cloned().unwrap_or_default(); + out.insert("bodies".into(), json!(bodies)); + Value::Object(out) + } selection::Target::Set | selection::Target::TargetSet => { let base = params.get("name").and_then(Value::as_str).map(|n| format!("{n}-set")); let set = self.selection_set(base.as_deref())?; diff --git a/crates/ui-egui/src/scene.rs b/crates/ui-egui/src/scene.rs index 8515db1..e11e1c2 100644 --- a/crates/ui-egui/src/scene.rs +++ b/crates/ui-egui/src/scene.rs @@ -194,7 +194,7 @@ mod tests { let mut d = DisplayMesh::default(); // Two edges of 10 mm; the first in two segments. d.add_curves(&[vec![[0.0; 3], [4.0, 0.0, 0.0], [10.0, 0.0, 0.0]], vec![[0.0; 3], [0.0, 10.0, 0.0]]]); - let names = [crate::selection::BodyNames { name: "B".into(), faces: vec![], edges: vec!["a|b".into(), "a|c".into()] }]; + let names = [crate::selection::BodyNames { name: "B".into(), faces: vec![], edges: vec!["a|b".into(), "a|c".into()], vertices: Vec::new() }]; let mut m = Model::default(); let edge = GeometryRef::new("B", Entity::Edge("a|b".into())).unwrap(); m.set_seed(Seed::new(edge, Divisions::Count(4), Some(Bias { ratio: 8.0, toward: BiasToward::Start }), SeedConstraint::Exact).unwrap()) diff --git a/crates/ui-egui/src/selection.rs b/crates/ui-egui/src/selection.rs index e137505..7d910ff 100644 --- a/crates/ui-egui/src/selection.rs +++ b/crates/ui-egui/src/selection.rs @@ -264,12 +264,13 @@ pub fn items_from_params(p: &Value) -> Result, String> { } /// A drawn geometry surface's names: its body and, by face tag, its faces; by edge index, its -/// B-rep edges ([`DisplayMesh::curves`]). -#[derive(Clone, Debug, Default, PartialEq, Eq)] +/// B-rep edges ([`DisplayMesh::curves`]); its B-rep vertices with their points. +#[derive(Clone, Debug, Default, PartialEq)] pub struct BodyNames { pub name: String, pub faces: Vec, pub edges: Vec, + pub vertices: Vec<(String, Vec3)>, } impl BodyNames { @@ -278,6 +279,11 @@ impl BodyNames { GeometryRef::face(self.name.clone(), self.faces.get(face as usize)?.clone()).ok() } + /// The reference to B-rep vertex `name` of this body. + fn vertex(&self, name: &str) -> Option { + GeometryRef::new(self.name.clone(), Entity::Vertex(name.to_string())).ok() + } + /// The reference to B-rep edge `edge` of this body, when it has a name. pub fn edge(&self, edge: u32) -> Option { GeometryRef::new(self.name.clone(), Entity::Edge(self.edges.get(edge as usize)?.clone())).ok() @@ -500,6 +506,14 @@ impl Picker<'_> { self.nearest_visible(proj, near, POINT_RADIUS).and_then(|i| self.display.nodes.get(i)).map(|n| Item::Node(n.label)) } Filter::Vertices => { + // A design's B-rep vertices first, by their persistent names. + if self.curves { + let design: Vec<(&BodyNames, &(String, Vec3))> = self.names.iter().flat_map(|b| b.vertices.iter().map(move |v| (b, v))).collect(); + let near = design.iter().enumerate().filter_map(|(i, (_, (_, p)))| Some((i, screen(*p)?, *p))).collect(); + if let Some((b, (name, _))) = nearest_passing(near, POINT_RADIUS, |p| self.curve_visible(proj, p)).and_then(|i| design.get(i)) { + return b.vertex(name).map(Item::Geometry); + } + } let tagged = &self.index.vertices; let near = tagged.iter().enumerate().filter_map(|(i, (n, _))| { let p = self.display.node_position(*n)?; @@ -692,6 +706,15 @@ impl Picker<'_> { } } } + if m.vertices && self.curves { + for body in self.names { + for (name, p) in &body.vertices { + if body.vertex(name).is_some_and(|g| m.refs.iter().any(|r| r.selects(&g))) { + s.points.push(ScenePoint { p: *p, color, size: NODE_SIZE }); + } + } + } + } let vertices = self.index.vertices.iter().filter(|_| m.vertices).filter(|(_, t)| on(Some(*t))).map(|(n, _)| *n); for n in m.nodes.iter().copied().chain(vertices) { if let Some(p) = self.display.node_position(n) { @@ -798,6 +821,15 @@ pub enum Target { Set, /// The selected geometry edges, as `edges: [{body, edge}]` (edge seeds). Edges, + /// A point at what is selected (`geometry.centre`: a vertex, a node, a hole's centre, a + /// centroid), as the reference point's `xyz`. + Position, + /// A datum point on what is selected: at a vertex, along an edge, at a face's centre, or at + /// the point `geometry.centre` finds (a hole's centre, nodes, several items). + DatumPoint, + /// The selected bodies (or the bodies of the selected faces, edges and vertices) as the + /// `bodies` list (the cells of a partitioned mesh). + Bodies, } /// Where command `id`'s form takes the selection, if it does. `params` (the form's current @@ -806,8 +838,13 @@ pub fn target(id: &str, params: &Value) -> Option { match id { "model.set.create" | "model.set.edit" => Some(Target::Members), "mesh.seed.edge" | "mesh.seed.clear" => Some(Target::Edges), + "model.reference_point.create" | "model.reference_point.edit" => Some(Target::Position), + "geometry.datum.point" | "geometry.datum.point.edit" => Some(Target::DatumPoint), + "mesh.cells" => Some(Target::Bodies), "model.boundary_condition.create" | "model.boundary_condition.edit" => Some(Target::TargetSet), - "model.section.create" | "model.section.edit" | "model.coupling.create" | "model.coupling.edit" => Some(Target::Set), + "model.section.create" | "model.section.edit" | "model.coupling.create" | "model.coupling.edit" | "interaction.rigid_wall" => { + Some(Target::Set) + } "model.load.create" | "model.load.edit" => { let kind = params.get("type").and_then(Value::as_str); Some(if matches!(kind, None | Some("force" | "moment")) { Target::TargetSet } else { Target::Set }) @@ -874,7 +911,7 @@ pub fn with_set(params: &Value, target: Target, set: &str) -> Value { Target::Set => { out.insert("set".into(), json!(set)); } - Target::Members | Target::Edges => {} + Target::Members | Target::Edges | Target::Position | Target::DatumPoint | Target::Bodies => {} } Value::Object(out) } @@ -888,6 +925,38 @@ pub fn same_set(model: &Model, set: &Value) -> Option { model.sets().iter().find(|(_, s)| **s == wanted).map(|(n, _)| n.clone()) } +/// The bodies of the selection's geometry (a body, or the body of a face, edge or vertex), each +/// once; `None` when no geometry is selected. +pub fn bodies(selection: &Selection) -> Option> { + let mut out: Vec = Vec::new(); + for item in selection.items() { + if let Item::Geometry(g) = item + && !out.contains(&g.body) + { + out.push(g.body.clone()); + } + } + (!out.is_empty()).then_some(out) +} + +/// A datum point's `by` for the selection, given the point `geometry.centre` found for it +/// (`centre`: its reply): one vertex, one edge or one face by name, unless the centre is a +/// hole's (`from: circle`); anything else at the centre's coordinates. +pub fn datum_point_by(selection: &Selection, centre: &Value) -> Value { + let circle = centre.get("from").and_then(Value::as_str) == Some("circle"); + let point = centre.get("point").cloned().unwrap_or(Value::Null); + let items = selection.items(); + match items.iter().next() { + Some(Item::Geometry(g)) if items.len() == 1 && !circle => match &g.entity { + Entity::Vertex(v) => json!({"type": "vertex", "body": g.body, "vertex": v}), + Entity::Edge(e) => json!({"type": "edge", "body": g.body, "edge": e, "fraction": 0.5}), + Entity::Face(f) => json!({"type": "face_centre", "body": g.body, "face": f}), + Entity::Body => json!({"type": "coordinates", "point": point}), + }, + _ => json!({"type": "coordinates", "point": point}), + } +} + /// A set name not taken in `model`: ``, then `-2`, `-3`, …; `Set` when /// `base` is not a valid name. pub fn fresh_set_name(model: &Model, base: &str) -> String { @@ -1062,7 +1131,8 @@ mod tests { // Its one B-rep edge: the diagonal between the two faces. display.add_curves(&[vec![square[0], square[2]]]); display.add_surface(&square, &[[0, 1, 2], [0, 2, 3]], &[0, 1]); - let names = [BodyNames { name: "Plate".into(), faces: vec!["F1:a".into(), "F1:b".into()], edges: vec!["F1:a|F1:b".into()] }]; + let names = + [BodyNames { name: "Plate".into(), faces: vec!["F1:a".into(), "F1:b".into()], edges: vec!["F1:a|F1:b".into()], vertices: Vec::new() }]; let model = Model::default(); let index = Index::new(&model, &display); let p = Picker { display: &display, names: &names, model: &model, index: &index, elements: false, geometry: true, curves: true };