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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-15 09:27:39 -04:00
1 parent 1372094f8a
commit a09e3c856b
5 files changed
+143 -8

No files matched your search

+24 -2
View File
@@ -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<Act>) {
];
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<Act>) {
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<Act>) {
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);
}
+22
View File
@@ -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 {
+21
View File
@@ -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<egui::Pos2>,
/// 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(&params, &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, &centre));
}
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())?;
+1 -1
View File
@@ -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())
+75 -5
View File
@@ -264,12 +264,13 @@ pub fn items_from_params(p: &Value) -> Result<Vec<Item>, 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<String>,
pub edges: Vec<String>,
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> {
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> {
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<Target> {
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<String> {
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<Vec<String>> {
let mut out: Vec<String> = 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`: `<base>`, then `<base>-2`, `<base>-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 };