Files
OneCAD-Simulation/crates/ui-egui/src/selection.rs
T
jyvikoandClaude Opus 5.5 a09e3c856b 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
2026-10-15 09:27:39 -04:00

1278 lines
57 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Selection by picking in the viewport.
//!
//! A click picks what is under the pointer through the selection filter: a geometry face, edge,
//! vertex or body, or a mesh node, element face or element. A box drag picks every visible node,
//! element face or element inside it. What is picked is kept in the model's persistent terms
//! ([`Item`]): geometry by name ([`GeometryRef`], body and face name), so a selection survives a
//! remesh, and the mesh by label. A [`Selection`]'s members are a set definition
//! ([`Selection::params`]), which is how command forms and `model.set.create` take it.
//!
//! Picking runs on the CPU against the display data the viewport draws (the GPU draws the same
//! triangles, so both renderers show what a pick finds): a ray through the pointer's pixel finds
//! the first face it hits with the watertight test of [`fieldcraft_render::pick`]; nodes, vertices
//! and edges are the nearest on screen within a few pixels whose own ray reaches them before any
//! face. Every choice is the nearest, ties going to the lower index, so a pick is deterministic.
//!
//! Geometry is named through the mesh's geometry tags ([`fieldcraft_model::Classification`]) once
//! the model is meshed, and through the drawn bodies' face names before. A design's edges are
//! picked on its drawn B-rep edges by their persistent names (`<face>|<face>`, before and after
//! meshing); other edges and vertices have names only on a mesh that carries them (block and
//! planar meshes).
use std::collections::{BTreeMap, BTreeSet};
use fieldcraft_model::{ElementEdge, ElementFace, Entity, GeometryRef, Model, Set, check_name};
use fieldcraft_render::pick::{DepthMap, Projector, segment_distance};
use fieldcraft_render::{DisplayMesh, Rgb, Scene, SceneLine, ScenePoint, SceneTri, Vec3};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
/// Most items a selection holds; a box or a list beyond it is cut there (and says so).
pub const MAX_SELECTION: usize = 100_000;
/// How far (pixels) from a node or vertex, and from an edge, a click still picks it.
const POINT_RADIUS: f64 = 8.0;
const EDGE_RADIUS: f64 = 6.0;
/// The nearest candidates on screen whose visibility is checked with a ray of their own.
const VISIBILITY_CHECKS: usize = 16;
/// Width of a highlighted edge and size of a highlighted node (points).
const EDGE_WIDTH: f64 = 4.0;
const NODE_SIZE: f64 = 9.0;
/// Width of the outline around highlighted faces (points).
const OUTLINE_WIDTH: f64 = 2.5;
/// What a pick selects.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Filter {
/// Geometry faces.
#[default]
Faces,
Edges,
Vertices,
Bodies,
/// Mesh nodes.
Nodes,
ElementFaces,
Elements,
}
impl Filter {
pub const ALL: [Filter; 7] =
[Filter::Faces, Filter::Edges, Filter::Vertices, Filter::Bodies, Filter::Nodes, Filter::ElementFaces, Filter::Elements];
pub fn label(self) -> &'static str {
match self {
Filter::Faces => "Faces",
Filter::Edges => "Edges",
Filter::Vertices => "Vertices",
Filter::Bodies => "Bodies",
Filter::Nodes => "Nodes",
Filter::ElementFaces => "Element faces",
Filter::Elements => "Elements",
}
}
/// Geometry (named) rather than mesh (labelled) entities.
pub fn geometry(self) -> bool {
matches!(self, Filter::Faces | Filter::Edges | Filter::Vertices | Filter::Bodies)
}
/// Whether a box drag selects with this filter.
pub fn boxes(self) -> bool {
matches!(self, Filter::Nodes | Filter::ElementFaces | Filter::Elements)
}
}
/// One selected entity, in the model's persistent terms.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Item {
Geometry(GeometryRef),
Node(u32),
Element(u32),
Face(ElementFace),
}
impl Item {
/// The item in words: `face F2:top of body Body1`, `node 5`, `element 3 face 2`.
pub fn describe(&self) -> String {
match self {
Item::Geometry(g) => g.to_string(),
Item::Node(n) => format!("node {n}"),
Item::Element(e) => format!("element {e}"),
Item::Face(f) => format!("element {} face {}", f.element, f.face),
}
}
/// The item as JSON: a geometry reference (`{"body", "face"}`), `{"node"}`, `{"element"}` or
/// `{"element", "face"}`.
pub fn to_json(&self) -> Value {
match self {
Item::Geometry(g) => json!(g),
Item::Node(n) => json!({"node": n}),
Item::Element(e) => json!({"element": e}),
Item::Face(f) => json!(f),
}
}
}
/// How picked items change the selection.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
/// The picked items become the selection (a plain click).
#[default]
Replace,
/// Added (Shift).
Add,
Remove,
/// Added, or removed when already selected (Cmd/Ctrl).
Toggle,
}
/// The selected items, sorted and without repeats, at most [`MAX_SELECTION`].
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Selection {
items: BTreeSet<Item>,
/// Items were left out to stay within [`MAX_SELECTION`].
capped: bool,
}
impl Selection {
pub fn items(&self) -> &BTreeSet<Item> {
&self.items
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn capped(&self) -> bool {
self.capped
}
pub fn clear(&mut self) {
self.items.clear();
self.capped = false;
}
/// Change the selection by `items` as `mode` says; anything past [`MAX_SELECTION`] is left
/// out and marks the selection capped. Returns whether the items changed.
pub fn apply(&mut self, items: impl IntoIterator<Item = Item>, mode: Mode) -> bool {
let mut changed = false;
if mode == Mode::Replace && !self.is_empty() {
// A replacement counts as a change, even by the same items.
changed = true;
self.clear();
}
for item in items {
changed |= match mode {
Mode::Remove => self.items.remove(&item),
Mode::Toggle if self.items.remove(&item) => true,
_ if self.items.contains(&item) => false,
_ if self.items.len() >= MAX_SELECTION => {
self.capped = true;
false
}
_ => self.items.insert(item),
};
}
changed
}
/// Keep only the items `keep` accepts.
pub fn retain(&mut self, keep: impl FnMut(&Item) -> bool) {
self.items.retain(keep);
}
/// The selection as a set definition's members (`model.set.create`'s parameters without the
/// name): `geometry`, `nodes`, `elements` and `faces`, each only when it has members.
pub fn params(&self) -> Value {
let mut set = Set::default();
for item in &self.items {
match item {
Item::Geometry(g) => set.geometry.push(g.clone()),
Item::Node(n) => set.nodes.push(*n),
Item::Element(e) => set.elements.push(*e),
Item::Face(f) => set.faces.push(*f),
}
}
// A set's JSON form leaves out its empty lists.
serde_json::to_value(set).unwrap_or_else(|_| json!({}))
}
/// What is selected in a few words: `2 faces, 15 nodes`.
pub fn summary(&self) -> String {
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
for item in &self.items {
let kind = match item {
Item::Geometry(g) => g.entity.kind(),
Item::Node(_) => "node",
Item::Element(_) => "element",
Item::Face(_) => "element face",
};
*counts.entry(kind).or_default() += 1;
}
if counts.is_empty() {
return "nothing selected".into();
}
let plural = |kind: &str, n: usize| match (kind, n) {
(k, 1) => k.to_string(),
("body", _) => "bodies".into(),
("vertex", _) => "vertices".into(),
(k, _) => format!("{k}s"),
};
counts.iter().map(|(k, n)| format!("{n} {}", plural(k, *n))).collect::<Vec<_>>().join(", ")
}
}
/// The members a selection is made of: a set definition's (`geometry`, `nodes`, `elements`,
/// `faces`; element edges cannot be picked yet).
const MEMBERS: [&str; 4] = ["geometry", "nodes", "elements", "faces"];
/// Read set members (`geometry`, `nodes`, `elements`, `faces`, as [`Selection::params`] writes
/// them) as items, checked as `model.set.create` checks a set. Each list holds at most
/// [`MAX_SELECTION`] entries.
pub fn items_from_params(p: &Value) -> Result<Vec<Item>, String> {
let mut members = serde_json::Map::new();
for key in MEMBERS {
match p.get(key) {
None | Some(Value::Null) => {}
Some(Value::Array(a)) if a.len() <= MAX_SELECTION => {
members.insert(key.into(), Value::Array(a.clone()));
}
Some(_) => return Err(format!("`{key}` must be a list of at most {MAX_SELECTION} entries")),
}
}
if members.is_empty() {
return Ok(Vec::new());
}
let set: Set = serde_json::from_value(Value::Object(members)).map_err(|e| e.to_string())?;
// Empty lists are no members; anything else is checked as a set's members are.
if set.is_empty() {
return Ok(Vec::new());
}
let set = set.normalized().map_err(|e| e.to_string())?;
let mut items: Vec<Item> = set.geometry.into_iter().map(Item::Geometry).collect();
items.extend(set.nodes.into_iter().map(Item::Node));
items.extend(set.elements.into_iter().map(Item::Element));
items.extend(set.faces.into_iter().map(Item::Face));
Ok(items)
}
/// A drawn geometry surface's names: its body and, by face tag, its faces; by edge index, its
/// 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 {
/// The reference to face `face` of this body, when it has a name.
fn face(&self, face: u32) -> Option<GeometryRef> {
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()
}
}
/// Where the mesh's drawn primitives lie on the geometry: built once per model revision from the
/// geometry tags.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Index {
/// Per element face triangle: the geometry face it lies on (a boundary face's tag, or the
/// face a shell fills).
face_tag: Vec<Option<u32>>,
/// Per element face triangle: the entity its element fills (a body for a solid).
region_tag: Vec<Option<u32>>,
/// Per line element: the entity it fills (an edge).
line_tag: Vec<Option<u32>>,
/// Element edges on geometry edges: their corner nodes ([`edge_key`]) → tag.
edge_tag: BTreeMap<(u32, u32), u32>,
/// Nodes on geometry vertices, and the vertex's tag.
vertices: Vec<(u32, u32)>,
}
/// An edge's key, whichever way round its corner nodes come.
fn edge_key(a: u32, b: u32) -> (u32, u32) {
(a.min(b), a.max(b))
}
impl Index {
pub fn new(model: &Model, display: &DisplayMesh) -> Index {
let c = model.classification();
let dimension = |tag: u32| c.entity(tag).map(|e| e.entity.dimension());
let tags = |element: u32| c.elements().get(&element);
let region = |element: u32| tags(element).and_then(|t| t.region);
let face_tag = display
.tris
.iter()
.map(|t| tags(t.element).and_then(|g| g.faces.get(&t.face).copied().or(g.region.filter(|r| dimension(*r) == Some(2)))))
.collect();
let region_tag = display.tris.iter().map(|t| region(t.element)).collect();
let line_tag = display.lines.iter().map(|l| region(l.element).filter(|r| dimension(*r) == Some(1))).collect();
let mut edge_tag = BTreeMap::new();
for (&element, t) in c.elements() {
for (&edge, &tag) in &t.edges {
if let Some(nodes) = model.edge_nodes(ElementEdge { element, edge })
&& let (Some(&a), Some(&b)) = (nodes.first(), nodes.get(1))
{
edge_tag.entry(edge_key(a, b)).or_insert(tag);
}
}
}
let vertices = c.nodes().iter().filter(|(_, t)| dimension(**t) == Some(0)).map(|(n, t)| (*n, *t)).collect();
Index { face_tag, region_tag, line_tag, edge_tag, vertices }
}
}
/// What picking looks at: the drawn display data and the names behind it.
pub struct Picker<'a> {
/// What the viewport draws: the model's display data, or a results plot's.
pub display: &'a DisplayMesh,
pub names: &'a [BodyNames],
pub model: &'a Model,
pub index: &'a Index,
/// Element faces are drawn (and picked).
pub elements: bool,
/// Geometry surfaces are drawn (and picked).
pub geometry: bool,
/// The geometry's B-rep edges are drawn (and picked), over the mesh too.
pub curves: bool,
}
/// The candidate nearest on screen within `radius` whose point passes `visible`: candidates are
/// (index, screen distance, the point to check); the first of the nearest few that passes wins.
fn nearest_passing(mut near: Vec<(usize, f64, Vec3)>, radius: f64, visible: impl Fn(Vec3) -> bool) -> Option<usize> {
near.retain(|c| c.1 <= radius);
near.sort_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
near.into_iter().take(VISIBILITY_CHECKS).find(|c| visible(c.2)).map(|c| c.0)
}
/// The first drawn face along a ray.
#[derive(Clone, Copy, Debug, PartialEq)]
enum Surface {
/// An element face triangle ([`DisplayMesh::tris`]) and the distance.
Element(usize, f64),
/// A geometry surface triangle ([`DisplayMesh::surface_tris`]).
Geometry(usize, f64),
}
impl Surface {
fn t(self) -> f64 {
match self {
Surface::Element(_, t) | Surface::Geometry(_, t) => t,
}
}
}
impl Picker<'_> {
fn entity(&self, tag: u32) -> Option<&GeometryRef> {
self.model.classification().entity(tag)
}
/// The first drawn face a ray hits; an element face wins a tie with a geometry face.
fn surface(&self, o: Vec3, d: Vec3) -> Option<Surface> {
let e = self.elements.then(|| self.display.hit_element_face(o, d)).flatten().map(|h| Surface::Element(h.index, h.t));
let g = self.geometry.then(|| self.display.hit_surface(o, d)).flatten().map(|h| Surface::Geometry(h.index, h.t));
match (e, g) {
(Some(e), Some(g)) => Some(if g.t() < e.t() { g } else { e }),
(e, g) => e.or(g),
}
}
/// The drawn faces' triangles (for a depth map).
fn drawn(&self) -> impl Iterator<Item = [Vec3; 3]> + '_ {
let e = self.display.tris.iter().filter(|_| self.elements).map(|t| t.p);
e.chain(self.display.surface_tris.iter().filter(|_| self.geometry).map(|t| t.p))
}
/// How far a point may lie behind the first face along its ray and still be on it.
fn slack(&self) -> f64 {
let d = self.display.bounds.diagonal();
if d.is_finite() { (d * 1e-4).max(1e-9) } else { 1e-9 }
}
/// Whether `p` is in front: the ray through its pixel reaches it before any drawn face.
fn visible(&self, proj: &Projector, p: Vec3) -> bool {
self.visible_within(proj, p, self.slack())
}
/// Whether `p` is in front, counting it on the first face along its ray within `slack`.
fn visible_within(&self, proj: &Projector, p: Vec3, slack: f64) -> bool {
let Some([x, y, _]) = proj.screen(p) else { return false };
let (o, d) = proj.ray(x, y);
let along = (p - o).dot(d);
self.surface(o, d).is_none_or(|s| along <= s.t() + slack)
}
/// The candidate nearest on screen to `at` within `radius` that is visible ([`nearest_passing`]).
fn nearest_visible(&self, proj: &Projector, near: Vec<(usize, f64, Vec3)>, radius: f64) -> Option<usize> {
nearest_passing(near, radius, |p| self.visible(proj, p))
}
/// Whether a point of a B-rep edge is in front. The edge's polyline and the drawn faces are
/// two approximations of the same curve, which can lie apart by the tessellation's chord
/// error: a point still counts as on the first face within a hundredth of the model's size.
fn curve_visible(&self, proj: &Projector, p: Vec3) -> bool {
self.visible_within(proj, p, self.slack() * 100.0)
}
/// The design edge (a [`DisplayMesh::curves`] segment's) nearest `at` on screen, visible.
fn pick_curve(&self, proj: &Projector, at: [f64; 2]) -> Option<GeometryRef> {
if !self.curves {
return None;
}
let near = self.display.curves.iter().enumerate().filter_map(|(i, c)| {
let (sa, sb) = (proj.screen(c.a)?, proj.screen(c.b)?);
let (distance, k) = segment_distance(at, [sa[0], sa[1]], [sb[0], sb[1]]);
(distance <= EDGE_RADIUS).then(|| (i, distance, c.a + (c.b - c.a) * k))
});
let i = nearest_passing(near.collect(), EDGE_RADIUS, |p| self.curve_visible(proj, p))?;
let c = self.display.curves.get(i)?;
self.names.get(c.body as usize)?.edge(c.edge)
}
/// The geometry face of an element face triangle or a geometry triangle.
fn geometry_face(&self, s: Surface) -> Option<GeometryRef> {
match s {
Surface::Element(i, _) => self.entity((*self.index.face_tag.get(i)?)?).cloned(),
Surface::Geometry(i, _) => {
let t = self.display.surface_tris.get(i)?;
self.names.get(t.body as usize)?.face(t.face)
}
}
}
/// The body an element face triangle or a geometry triangle belongs to.
fn body(&self, s: Surface) -> Option<GeometryRef> {
let name = match s {
Surface::Element(i, _) => {
let tag = self.index.region_tag.get(i).copied().flatten().or(self.index.face_tag.get(i).copied().flatten())?;
self.entity(tag)?.body.clone()
}
Surface::Geometry(i, _) => self.names.get(self.display.surface_tris.get(i)?.body as usize)?.name.clone(),
};
GeometryRef::body(name).ok()
}
/// What is under pixel `at` with `filter`.
pub fn pick(&self, proj: &Projector, at: [f64; 2], filter: Filter) -> Option<Item> {
if !(at[0].is_finite() && at[1].is_finite()) {
return None;
}
let (o, d) = proj.ray(at[0], at[1]);
let screen = |p: Vec3| proj.screen(p).map(|s| ((s[0] - at[0]).powi(2) + (s[1] - at[1]).powi(2)).sqrt());
match filter {
Filter::Faces => self.geometry_face(self.surface(o, d)?).map(Item::Geometry),
Filter::Bodies => self.body(self.surface(o, d)?).map(Item::Geometry),
Filter::ElementFaces => match self.surface(o, d)? {
Surface::Element(i, _) => {
let t = self.display.tris.get(i)?;
Some(Item::Face(ElementFace { element: t.element, face: t.seen_face(d) }))
}
Surface::Geometry(..) => None,
},
Filter::Elements => {
let lines = self.display.lines.iter().enumerate().filter_map(|(i, l)| {
let (a, b) = (proj.screen(l.a)?, proj.screen(l.b)?);
let (distance, k) = segment_distance(at, [a[0], a[1]], [b[0], b[1]]);
Some((i, distance, l.a + (l.b - l.a) * k))
});
if let Some(l) = self.nearest_visible(proj, lines.collect(), EDGE_RADIUS).and_then(|i| self.display.lines.get(i)) {
return Some(Item::Element(l.element));
}
match self.surface(o, d)? {
Surface::Element(i, _) => self.display.tris.get(i).map(|t| Item::Element(t.element)),
Surface::Geometry(..) => None,
}
}
Filter::Nodes => {
let near = self.display.nodes.iter().enumerate().filter_map(|(i, n)| Some((i, screen(n.p)?, n.p))).collect();
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)?;
Some((i, screen(p)?, p))
});
let (_, tag) = tagged.get(self.nearest_visible(proj, near.collect(), POINT_RADIUS)?)?;
self.entity(*tag).cloned().map(Item::Geometry)
}
Filter::Edges => {
// A design's B-rep edges first: they are what an edge is named by before meshing,
// and what seeds and sets refer to after.
if let Some(g) = self.pick_curve(proj, at) {
return Some(Item::Geometry(g));
}
// Element edges and line elements on geometry edges.
let mut tagged: Vec<(Vec3, Vec3, u32)> = self
.display
.edges
.iter()
.filter_map(|e| Some((e.p[0], e.p[1], *self.index.edge_tag.get(&edge_key(e.corners[0], e.corners[1]))?)))
.collect();
tagged.extend(self.display.lines.iter().zip(&self.index.line_tag).filter_map(|(l, t)| Some((l.a, l.b, (*t)?))));
let near = tagged.iter().enumerate().filter_map(|(i, (a, b, _))| {
let (sa, sb) = (proj.screen(*a)?, proj.screen(*b)?);
// The edge's point nearest the pointer on screen is the one checked.
let (distance, k) = segment_distance(at, [sa[0], sa[1]], [sb[0], sb[1]]);
Some((i, distance, *a + (*b - *a) * k))
});
let (_, _, tag) = tagged.get(self.nearest_visible(proj, near.collect(), EDGE_RADIUS)?)?;
self.entity(*tag).cloned().map(Item::Geometry)
}
}
}
/// The visible nodes, element faces or elements (with `filter`) inside the screen box
/// `rect` ([x0, y0, x1, y1], any two opposite corners), sorted; other filters select
/// nothing. ([`Selection::apply`] keeps at most [`MAX_SELECTION`].)
pub fn pick_box(&self, proj: &Projector, rect: [f64; 4], filter: Filter) -> Vec<Item> {
if !filter.boxes() || !rect.iter().all(|v| v.is_finite()) {
return Vec::new();
}
let (x0, x1, y0, y1) = (rect[0].min(rect[2]), rect[0].max(rect[2]), rect[1].min(rect[3]), rect[1].max(rect[3]));
let inside = |p: Vec3| proj.screen(p).is_some_and(|s| (x0..=x1).contains(&s[0]) && (y0..=y1).contains(&s[1]));
let Some(depth) = DepthMap::new(proj, self.drawn()) else { return Vec::new() };
let mut out = BTreeSet::new();
let mut add = |item: Item| {
out.insert(item);
};
match filter {
Filter::Nodes => {
for n in self.display.nodes.iter().filter(|n| inside(n.p) && depth.visible(n.p)) {
add(Item::Node(n.label));
}
}
Filter::ElementFaces | Filter::Elements => {
let faces = filter == Filter::ElementFaces;
for t in self.display.tris.iter().filter(|_| self.elements) {
let centre = (t.p[0] + t.p[1] + t.p[2]) * (1.0 / 3.0);
if t.p.iter().all(|p| inside(*p)) && depth.visible(centre) {
add(if faces {
let dir = proj.screen(centre).map_or(Vec3::ZERO, |s| proj.ray(s[0], s[1]).1);
Item::Face(ElementFace { element: t.element, face: t.seen_face(dir) })
} else {
Item::Element(t.element)
});
}
}
if !faces {
for l in &self.display.lines {
if inside(l.a) && inside(l.b) && depth.visible((l.a + l.b) * 0.5) {
add(Item::Element(l.element));
}
}
}
}
_ => {}
}
out.into_iter().collect()
}
/// The selection and the hovered item drawn over the scene: their faces in `selected` (or
/// `hover`) colour, their edges as wide lines and their nodes as large points.
pub fn overlay(&self, selection: &Selection, hover: Option<&Item>, selected: Rgb, hover_color: Rgb) -> Scene {
let mut s = Scene::default();
self.mark(&Marks::of(self, selection.items().iter()), selected, &mut s);
if let Some(h) = hover {
self.mark(&Marks::of(self, std::iter::once(h)), hover_color, &mut s);
}
s
}
/// `items` (a set's members) drawn over the scene in `color`.
pub fn items_overlay(&self, items: &[Item], color: Rgb) -> Scene {
let mut s = Scene::default();
self.mark(&Marks::of(self, items.iter()), color, &mut s);
s
}
/// The hovered item drawn over the scene in `color`. With `fill` off (while result contours
/// are shown) its faces are left out and only their outline, edges and nodes are drawn, so
/// the hover never hides the colours under the pointer.
pub fn hover(&self, item: &Item, color: Rgb, fill: bool) -> Scene {
let mut s = Scene::default();
self.mark(&Marks::of(self, std::iter::once(item)), color, &mut s);
if !fill {
s.tris.clear();
}
s
}
fn mark(&self, m: &Marks, color: Rgb, s: &mut Scene) {
if m.is_empty() {
return;
}
let first = s.tris.len();
let flag = |flags: &[bool], tag: Option<u32>| tag.is_some_and(|t| flags.get(t as usize).copied().unwrap_or(false));
let on = |tag: Option<u32>| flag(&m.tags, tag);
let in_body = |tag: Option<u32>| flag(&m.in_bodies, tag);
if self.elements {
for (i, t) in self.display.tris.iter().enumerate() {
let (face, region) = (self.index.face_tag.get(i).copied().flatten(), self.index.region_tag.get(i).copied().flatten());
// A shell's triangle draws both of its sides.
let shell_back = t.kind.dimension() == 2 && m.faces.contains(&ElementFace { element: t.element, face: 2 });
if m.elements.contains(&t.element)
|| m.faces.contains(&ElementFace { element: t.element, face: t.face })
|| shell_back
|| on(face)
|| in_body(region)
|| in_body(face)
{
s.tris.push(SceneTri { p: t.p, normal: t.normal, color });
}
}
for (l, tag) in self.display.lines.iter().zip(&self.index.line_tag) {
if m.elements.contains(&l.element) || on(*tag) {
s.lines.push(SceneLine { a: l.a, b: l.b, color, width: EDGE_WIDTH });
}
}
}
if self.geometry && !m.refs.is_empty() {
// Which faces of each drawn body are marked, worked out once per face.
let marked: Vec<Vec<bool>> = self
.names
.iter()
.map(|body| {
(0..body.faces.len())
.map(|f| {
let face = body.face(u32::try_from(f).unwrap_or(u32::MAX));
m.refs.iter().any(|r| match &r.entity {
Entity::Body => r.body == body.name,
_ => face.as_ref().is_some_and(|g| r.selects(g)),
})
})
.collect()
})
.collect();
let whole = |b: usize| self.names.get(b).is_some_and(|n| m.bodies.contains(&n.name));
for t in &self.display.surface_tris {
let b = t.body as usize;
if whole(b) || marked.get(b).and_then(|f| f.get(t.face as usize)).copied().unwrap_or(false) {
s.tris.push(SceneTri { p: t.p, normal: t.normal, color });
}
}
}
outline(s, first, color);
// A design edge is marked along its own polyline, which the scene draws over the mesh too
// (most meshes do not tag their element edges with the design's edge names), and along
// the mesh's edges a mesh does tag with it (below).
if m.edges && self.curves {
// Which edges of each drawn body are marked, worked out once per edge.
let marked: Vec<Vec<bool>> = self
.names
.iter()
.map(|body| {
(0..body.edges.len())
.map(|e| body.edge(u32::try_from(e).unwrap_or(u32::MAX)).is_some_and(|g| m.refs.iter().any(|r| r.selects(&g))))
.collect()
})
.collect();
for c in &self.display.curves {
if marked.get(c.body as usize).and_then(|b| b.get(c.edge as usize)).copied().unwrap_or(false) {
s.lines.push(SceneLine { a: c.a, b: c.b, color, width: EDGE_WIDTH });
}
}
}
if m.edges {
for e in &self.display.edges {
if on(self.index.edge_tag.get(&edge_key(e.corners[0], e.corners[1])).copied()) {
s.lines.push(SceneLine { a: e.p[0], b: e.p[1], color, width: EDGE_WIDTH });
}
}
}
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) {
s.points.push(ScenePoint { p, color, size: NODE_SIZE });
}
}
}
}
/// The outline of the triangles `s.tris[first..]`: their sides no other of them shares, drawn as
/// lines so a highlight reads at a glance.
fn outline(s: &mut Scene, first: usize, color: Rgb) {
let key = |p: Vec3| [p.x, p.y, p.z].map(f64::to_bits);
let mut sides: Vec<([[u64; 3]; 2], Vec3, Vec3)> = Vec::new();
for t in s.tris.get(first..).unwrap_or(&[]) {
for (a, b) in [(t.p[0], t.p[1]), (t.p[1], t.p[2]), (t.p[2], t.p[0])] {
let (ka, kb) = (key(a), key(b));
sides.push((if ka < kb { [ka, kb] } else { [kb, ka] }, a, b));
}
}
sides.sort_by_key(|x| x.0);
for run in sides.chunk_by(|x, y| x.0 == y.0) {
if let [(_, a, b)] = run {
s.lines.push(SceneLine { a: *a, b: *b, color, width: OUTLINE_WIDTH });
}
}
}
/// What a group of items marks, for drawing.
#[derive(Default)]
struct Marks {
/// Geometry tags selected (by index).
tags: Vec<bool>,
/// Geometry tags of an entity of a body selected whole.
in_bodies: Vec<bool>,
/// Bodies selected whole.
bodies: BTreeSet<String>,
/// The geometry references (for the drawn geometry surfaces).
refs: Vec<GeometryRef>,
/// Whether an edge or a vertex is among the references.
edges: bool,
vertices: bool,
nodes: BTreeSet<u32>,
elements: BTreeSet<u32>,
faces: BTreeSet<ElementFace>,
}
impl Marks {
fn of<'a>(p: &Picker, items: impl Iterator<Item = &'a Item>) -> Marks {
let c = p.model.classification();
let n = c.entities().len();
let mut m = Marks { tags: vec![false; n], in_bodies: vec![false; n], ..Marks::default() };
for item in items {
match item {
Item::Geometry(g) => {
match g.entity {
Entity::Body => {
m.bodies.insert(g.body.clone());
}
Entity::Edge(_) => m.edges = true,
Entity::Vertex(_) => m.vertices = true,
Entity::Face(_) => {}
}
for t in c.tags_selected_by(g) {
if let Some(slot) = m.tags.get_mut(t as usize) {
*slot = true;
}
}
m.refs.push(g.clone());
}
Item::Node(n) => {
m.nodes.insert(*n);
}
Item::Element(e) => {
m.elements.insert(*e);
}
Item::Face(f) => {
m.faces.insert(*f);
}
}
}
if !m.bodies.is_empty() {
for (slot, e) in m.in_bodies.iter_mut().zip(c.entities()) {
*slot = m.bodies.contains(&e.body);
}
}
m
}
fn is_empty(&self) -> bool {
self.refs.is_empty() && self.nodes.is_empty() && self.elements.is_empty() && self.faces.is_empty()
}
}
/// Where a command form puts the selection.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Target {
/// The members themselves (a set's definition).
Members,
/// A set made from the selection, named in `target: {"set"}` (boundary conditions, forces).
TargetSet,
/// A set made from the selection, named in `set` (sections, couplings, surface and body
/// loads).
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
/// parameters) tells a force (target) from a pressure (set).
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" | "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 })
}
_ => None,
}
}
/// The filter command `id`'s form picks with, when its target is one kind of entity: edges for
/// edge seeds, and the vertex, edge or face a datum point is defined on.
pub fn form_filter(id: &str, params: &Value) -> Option<Filter> {
if target(id, params) == Some(Target::Edges) {
return Some(Filter::Edges);
}
match id {
"geometry.datum.point" | "geometry.datum.point.edit" => match params.pointer("/by/type").and_then(Value::as_str) {
Some("vertex") => Some(Filter::Vertices),
Some("edge") => Some(Filter::Edges),
Some("face_centre") => Some(Filter::Faces),
_ => None,
},
_ => None,
}
}
/// `params` with the selection's members put in (replacing any members it had).
pub fn with_members(params: &Value, selection: &Selection) -> Value {
let mut out = params.as_object().cloned().unwrap_or_default();
for key in ["geometry", "nodes", "elements", "faces", "edges"] {
out.remove(key);
}
if let Value::Object(members) = selection.params() {
out.extend(members);
}
Value::Object(out)
}
/// `params` with the selection's geometry edges as `edges` (replacing any it had); `None` when no
/// edge is selected.
pub fn with_edges(params: &Value, selection: &Selection) -> Option<Value> {
let edges: Vec<Value> = selection
.items()
.iter()
.filter_map(|i| match i {
Item::Geometry(g) if matches!(g.entity, Entity::Edge(_)) => Some(json!(g)),
_ => None,
})
.collect();
if edges.is_empty() {
return None;
}
let mut out = params.as_object().cloned().unwrap_or_default();
out.insert("edges".into(), Value::Array(edges));
Some(Value::Object(out))
}
/// `params` with the set `set` as the target.
pub fn with_set(params: &Value, target: Target, set: &str) -> Value {
let mut out = params.as_object().cloned().unwrap_or_default();
match target {
Target::TargetSet => {
out.insert("target".into(), json!({"set": set}));
}
Target::Set => {
out.insert("set".into(), json!(set));
}
Target::Members | Target::Edges | Target::Position | Target::DatumPoint | Target::Bodies => {}
}
Value::Object(out)
}
/// The set of `model` whose members are exactly those of `set` (`model.set.create`'s
/// parameters), if any.
pub fn same_set(model: &Model, set: &Value) -> Option<String> {
let mut members = set.as_object().cloned()?;
members.remove("name");
let wanted = serde_json::from_value::<Set>(Value::Object(members)).ok()?.normalized().ok()?;
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 {
let base = if check_name("set", base).is_ok() { base } else { "Set" };
std::iter::once(base.to_string())
.chain((2..100_000).map(|k| format!("{base}-{k}")))
.find(|n| check_name("set", n).is_ok() && model.set(n).is_none())
.unwrap_or_else(|| "Set".into())
}
#[cfg(test)]
mod tests {
use fieldcraft_model::ElementKind;
use fieldcraft_render::{Camera, StandardView};
use super::*;
/// `nx × ny × nz` unit hex8 cells, the faces at x = nx tagged as face `end` of body `Bar`, the
/// solids as the body, the edge along x = nx, z = nz as edge `top` and the corner node at
/// (nx, 0, 0) as vertex `v`.
fn bar(nx: u32, ny: u32, nz: u32) -> Model {
let mut m = Model::default();
let id = |i: u32, j: u32, k: u32| 1 + i + j * (nx + 1) + k * (nx + 1) * (ny + 1);
for k in 0..=nz {
for j in 0..=ny {
for i in 0..=nx {
m.set_node(id(i, j, k), [i as f64, j as f64, k as f64]).unwrap();
}
}
}
let body = m.geometry_tag(&GeometryRef::body("Bar").unwrap()).unwrap();
let end = m.geometry_tag(&GeometryRef::face("Bar", "end").unwrap()).unwrap();
let top = m.geometry_tag(&GeometryRef::new("Bar", Entity::Edge("top".into())).unwrap()).unwrap();
let v = m.geometry_tag(&GeometryRef::new("Bar", Entity::Vertex("v".into())).unwrap()).unwrap();
let mut label = 1;
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
let n = vec![
id(i, j, k),
id(i + 1, j, k),
id(i + 1, j + 1, k),
id(i, j + 1, k),
id(i, j, k + 1),
id(i + 1, j, k + 1),
id(i + 1, j + 1, k + 1),
id(i, j + 1, k + 1),
];
m.set_element(label, ElementKind::Hex8, n).unwrap();
m.tag_element(label, body).unwrap();
if i + 1 == nx {
// Hex8 face 4: corners 1, 2, 6, 5 (x = i + 1).
m.tag_element_face(label, 4, end).unwrap();
if k + 1 == nz {
// Hex8 edge 6 joins corners 5 and 6 (x = i + 1, z = k + 1).
m.tag_element_edge(label, 6, top).unwrap();
}
}
label += 1;
}
}
}
m.tag_node(id(nx, 0, 0), v).unwrap();
m
}
struct Scene3 {
model: Model,
display: DisplayMesh,
index: Index,
}
impl Scene3 {
fn new(model: Model) -> Scene3 {
let display = DisplayMesh::from_model(&model);
let index = Index::new(&model, &display);
Scene3 { model, display, index }
}
fn picker(&self) -> Picker<'_> {
Picker { display: &self.display, names: &[], model: &self.model, index: &self.index, elements: true, geometry: false, curves: false }
}
}
/// A front-on view of the bar's end (looking along −x from +x), 400 × 300 pixels.
fn end_view(d: &DisplayMesh) -> Projector {
let mut cam = Camera::default();
cam.set_view(StandardView::Right);
cam.fit(&d.bounds);
Projector::new(&cam, 400.0, 300.0, d.bounds.diagonal())
}
fn pixel(p: &Projector, at: Vec3) -> [f64; 2] {
let s = p.screen(at).unwrap();
[s[0], s[1]]
}
#[test]
fn a_click_on_the_end_picks_its_face_body_element_face_element_and_node() {
let s = Scene3::new(bar(4, 2, 2));
let p = s.picker();
let proj = end_view(&s.display);
// The centre of the end cell at y ∈ [0, 1], z ∈ [1, 2]: element 1 + 3 + 0·4 + 1·8 = 12.
let at = pixel(&proj, Vec3::new(4.0, 0.5, 1.5));
assert_eq!(p.pick(&proj, at, Filter::Faces), Some(Item::Geometry(GeometryRef::face("Bar", "end").unwrap())));
assert_eq!(p.pick(&proj, at, Filter::Bodies), Some(Item::Geometry(GeometryRef::body("Bar").unwrap())));
assert_eq!(p.pick(&proj, at, Filter::ElementFaces), Some(Item::Face(ElementFace { element: 12, face: 4 })));
assert_eq!(p.pick(&proj, at, Filter::Elements), Some(Item::Element(12)));
// Near the corner (4, 1, 2), its node; far from every node, nothing.
let corner = pixel(&proj, Vec3::new(4.0, 1.0, 2.0));
let n = p.pick(&proj, [corner[0] + 3.0, corner[1] - 2.0], Filter::Nodes);
assert_eq!(n, Some(Item::Node(1 + 4 + 5 + 2 * 15)));
assert_eq!(p.pick(&proj, at, Filter::Nodes), None);
// The tagged vertex and edge.
let v = pixel(&proj, Vec3::new(4.0, 0.0, 0.0));
assert_eq!(p.pick(&proj, v, Filter::Vertices), Some(Item::Geometry(GeometryRef::new("Bar", Entity::Vertex("v".into())).unwrap())));
let e = pixel(&proj, Vec3::new(4.0, 1.5, 2.0));
assert_eq!(
p.pick(&proj, [e[0], e[1] + 2.0], Filter::Edges),
Some(Item::Geometry(GeometryRef::new("Bar", Entity::Edge("top".into())).unwrap()))
);
// Off the model, and hostile pixels.
assert_eq!(p.pick(&proj, [2.0, 2.0], Filter::Faces), None);
assert_eq!(p.pick(&proj, [f64::NAN, 1.0], Filter::Nodes), None);
assert_eq!(p.pick(&proj, [1e300, -1e300], Filter::Edges), None);
}
/// Nodes behind the end face are not picked through it, even right under the pointer.
#[test]
fn hidden_nodes_are_not_picked() {
let s = Scene3::new(bar(4, 2, 2));
let p = s.picker();
let mut cam = Camera::default();
cam.set_view(StandardView::Right);
cam.fit(&s.display.bounds);
// Orthographic along −x: the node (3, 1, 1) projects onto (4, 1, 1), which is in front.
let proj = Projector::new(&cam, 400.0, 300.0, s.display.bounds.diagonal());
let at = pixel(&proj, Vec3::new(4.0, 1.0, 1.0));
let front = 1 + 4 + 5 + 15;
assert_eq!(p.pick(&proj, at, Filter::Nodes), Some(Item::Node(front)));
let items = p.pick_box(&proj, [0.0, 0.0, 400.0, 300.0], Filter::Nodes);
assert_eq!(items.len(), 3 * 3, "the end's nodes only: {items:?}");
assert!(items.contains(&Item::Node(front)));
// Element faces and elements in the box: the end's eight cells.
let faces = p.pick_box(&proj, [0.0, 0.0, 400.0, 300.0], Filter::ElementFaces);
assert_eq!(faces.len(), 4);
assert!(faces.iter().all(|f| matches!(f, Item::Face(ElementFace { face: 4, .. }))));
let elements = p.pick_box(&proj, [400.0, 300.0, 0.0, 0.0], Filter::Elements);
assert_eq!(elements, vec![Item::Element(4), Item::Element(8), Item::Element(12), Item::Element(16)]);
// Filters without boxes, empty and hostile boxes.
assert_eq!(p.pick_box(&proj, [0.0, 0.0, 400.0, 300.0], Filter::Faces), vec![]);
assert_eq!(p.pick_box(&proj, [5.0, 5.0, 6.0, 6.0], Filter::Nodes), vec![]);
assert_eq!(p.pick_box(&proj, [f64::NAN, 0.0, 1.0, 1.0], Filter::Nodes), vec![]);
}
#[test]
fn an_empty_scene_picks_nothing() {
let s = Scene3::new(Model::default());
let p = s.picker();
let proj = Projector::new(&Camera::default(), 400.0, 300.0, 1.0);
for f in Filter::ALL {
assert_eq!(p.pick(&proj, [200.0, 150.0], f), None);
assert_eq!(p.pick_box(&proj, [0.0, 0.0, 400.0, 300.0], f), vec![]);
}
assert!(p.overlay(&Selection::default(), None, Rgb(1, 2, 3), Rgb(4, 5, 6)).tris.is_empty());
}
/// Geometry surfaces (no mesh) are named by their bodies' face names.
#[test]
fn drawn_geometry_names_its_faces() {
let mut display = DisplayMesh::default();
let square = [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.]];
// 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()], 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 };
let mut cam = Camera::default();
cam.set_view(StandardView::Top);
cam.fit(&display.bounds);
let proj = Projector::new(&cam, 400.0, 300.0, 2.0);
let at = pixel(&proj, Vec3::new(0.8, 0.2, 0.0));
let face = GeometryRef::face("Plate", "F1:a").unwrap();
assert_eq!(p.pick(&proj, at, Filter::Faces), Some(Item::Geometry(face.clone())));
assert_eq!(p.pick(&proj, at, Filter::Bodies), Some(Item::Geometry(GeometryRef::body("Plate").unwrap())));
assert_eq!(p.pick(&proj, at, Filter::ElementFaces), None);
// The face is highlighted: its one triangle in the selection colour.
let mut sel = Selection::default();
sel.apply([Item::Geometry(face)], Mode::Replace);
let o = p.overlay(&sel, None, Rgb(9, 9, 9), Rgb(0, 0, 0));
assert_eq!(o.tris.len(), 1);
assert_eq!(o.tris[0].color, Rgb(9, 9, 9));
// The design edge is picked by its persistent name, and drawn wide when selected.
let edge = GeometryRef::new("Plate", Entity::Edge("F1:a|F1:b".into())).unwrap();
let on = pixel(&proj, Vec3::new(0.5, 0.5, 0.0));
assert_eq!(p.pick(&proj, on, Filter::Edges), Some(Item::Geometry(edge.clone())));
assert_eq!(p.pick(&proj, at, Filter::Edges), None);
sel.apply([Item::Geometry(edge)], Mode::Replace);
let o = p.overlay(&sel, None, Rgb(9, 9, 9), Rgb(0, 0, 0));
assert_eq!((o.tris.len(), o.lines.len()), (0, 1));
// Without the curves drawn, the edge is not picked.
let hidden = Picker { curves: false, ..p };
assert_eq!(hidden.pick(&proj, on, Filter::Edges), None);
}
#[test]
fn the_overlay_marks_what_is_selected_and_hovered() {
let s = Scene3::new(bar(4, 2, 2));
let p = s.picker();
let (sel_c, hov_c) = (Rgb(200, 0, 0), Rgb(0, 200, 0));
let mut sel = Selection::default();
sel.apply([Item::Geometry(GeometryRef::face("Bar", "end").unwrap())], Mode::Replace);
let o = p.overlay(&sel, None, sel_c, hov_c);
// Four quads at the end, two triangles each.
assert_eq!(o.tris.len(), 8);
assert!(o.tris.iter().all(|t| t.p.iter().all(|q| q.x == 4.0)));
// Hovering an element adds its skin triangles in the hover colour, drawn after.
let o = p.overlay(&sel, Some(&Item::Element(1)), sel_c, hov_c);
assert_eq!(o.tris.iter().filter(|t| t.color == hov_c).count(), 6);
// Over result contours the hover is its outline only: no face fills.
let filled = p.hover(&Item::Element(1), hov_c, true);
assert_eq!(filled.tris.len(), 6);
let outlined = p.hover(&Item::Element(1), hov_c, false);
assert!(outlined.tris.is_empty());
assert_eq!(outlined.lines, filled.lines);
assert!(!outlined.lines.is_empty());
// Nodes, the edge and the vertex.
sel.apply(
[
Item::Node(1),
Item::Geometry(GeometryRef::new("Bar", Entity::Edge("top".into())).unwrap()),
Item::Geometry(GeometryRef::new("Bar", Entity::Vertex("v".into())).unwrap()),
],
Mode::Replace,
);
let o = p.overlay(&sel, None, sel_c, hov_c);
assert_eq!((o.tris.len(), o.lines.len(), o.points.len()), (0, 2, 2));
// A whole body: every skin triangle.
sel.apply([Item::Geometry(GeometryRef::body("Bar").unwrap())], Mode::Replace);
assert_eq!(p.overlay(&sel, None, sel_c, hov_c).tris.len(), s.display.tris.len());
}
#[test]
fn selections_add_remove_toggle_and_cap() {
let mut s = Selection::default();
s.apply([Item::Node(3), Item::Node(1), Item::Node(3)], Mode::Replace);
assert_eq!(s.len(), 2);
s.apply([Item::Element(7)], Mode::Add);
s.apply([Item::Node(1), Item::Node(9)], Mode::Toggle);
assert_eq!(s.items().iter().cloned().collect::<Vec<_>>(), vec![Item::Node(3), Item::Node(9), Item::Element(7)]);
s.apply([Item::Node(3)], Mode::Remove);
assert_eq!(s.summary(), "1 element, 1 node");
s.apply((1..=MAX_SELECTION as u32 + 10).map(Item::Node), Mode::Replace);
assert!(s.len() == MAX_SELECTION && s.capped());
s.clear();
assert!(s.is_empty() && !s.capped() && s.summary() == "nothing selected");
}
/// The selection becomes a set's members, and reads back from them.
#[test]
fn the_selection_maps_to_set_members_and_command_parameters() {
let mut s = Selection::default();
let face = GeometryRef::face("Body1", "F2:top").unwrap();
s.apply([Item::Geometry(face.clone()), Item::Node(5), Item::Element(2), Item::Face(ElementFace { element: 2, face: 3 })], Mode::Replace);
let p = s.params();
assert_eq!(
p,
json!({"geometry": [{"body": "Body1", "face": "F2:top"}], "nodes": [5], "elements": [2], "faces": [{"element": 2, "face": 3}]})
);
assert_eq!(items_from_params(&p).unwrap().into_iter().collect::<BTreeSet<_>>(), *s.items());
assert_eq!(s.summary(), "1 element, 1 element face, 1 face, 1 node");
// A set's form: the members replace the old ones, the name stays.
let form = with_members(&json!({"name": "end", "nodes": [1, 2], "edges": [{"element": 1, "edge": 1}]}), &s);
assert_eq!(form["name"], "end");
assert_eq!(form["nodes"], json!([5]));
assert!(form.get("edges").is_none());
// Definitions that take a set.
assert_eq!(target("model.set.create", &json!({})), Some(Target::Members));
assert_eq!(target("model.boundary_condition.create", &json!({})), Some(Target::TargetSet));
assert_eq!(target("model.load.create", &json!({"type": "force"})), Some(Target::TargetSet));
assert_eq!(target("model.load.create", &json!({"type": "pressure"})), Some(Target::Set));
assert_eq!(target("model.section.edit", &json!({})), Some(Target::Set));
assert_eq!(target("model.coupling.create", &json!({})), Some(Target::Set));
assert_eq!(target("mesh.node.add", &json!({})), None);
assert_eq!(form_filter("mesh.seed.edge", &json!({})), Some(Filter::Edges));
assert_eq!(form_filter("geometry.datum.point", &json!({"by": {"type": "vertex"}})), Some(Filter::Vertices));
assert_eq!(form_filter("model.load.create", &json!({})), None);
assert_eq!(with_set(&json!({"name": "fix"}), Target::TargetSet, "fix-set"), json!({"name": "fix", "target": {"set": "fix-set"}}));
assert_eq!(with_set(&json!({"name": "web"}), Target::Set, "web-set"), json!({"name": "web", "set": "web-set"}));
let mut m = Model::default();
assert_eq!(fresh_set_name(&m, "fix-set"), "fix-set");
m.add_set("fix-set", fieldcraft_model::Set { nodes: vec![1], ..Default::default() }).unwrap();
assert_eq!(fresh_set_name(&m, "fix-set"), "fix-set-2");
assert_eq!(fresh_set_name(&m, " \u{7}"), "Set");
assert_eq!(fresh_set_name(&m, "my clamp-set"), "Set");
}
#[test]
fn hostile_members_are_refused() {
for bad in [
json!({"nodes": [0]}),
json!({"nodes": "x"}),
json!({"nodes": [-1]}),
json!({"elements": [1.5]}),
json!({"faces": [{"element": 1, "face": 0}]}),
json!({"faces": [{"element": 0, "face": 1}]}),
json!({"geometry": [{"body": ""}]}),
json!({"geometry": [{"body": "B", "face": "f", "edge": "e"}]}),
json!({"nodes": vec![1; MAX_SELECTION + 1]}),
] {
assert!(items_from_params(&bad).is_err(), "{bad}");
}
assert_eq!(items_from_params(&json!({})).unwrap(), vec![]);
assert_eq!(items_from_params(&json!(null)).unwrap(), vec![]);
}
}