forked from 1b-agent/fieldcraft-upstream
UI: pick a design's edges by their persistent names
The viewport keeps each drawn body's B-rep edges; with Select: Edges a click picks the nearest visible one (before and after meshing), hover and the selection draw it wide, and multi-select works as for faces. Part of #230. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
ffb71de6e1
commit
94c98e0c0b
5 files changed
+139
-19
No files matched your search
@@ -755,7 +755,8 @@ mod tests {
|
||||
// A tagged tetrahedron, and the same with a face missing.
|
||||
let p = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
||||
let t = vec![[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]];
|
||||
let tet = SurfaceMesh { positions: p.clone(), triangles: t.clone(), tri_face: vec![0, 1, 1, 1], weld_distance: 0.0, ..SurfaceMesh::default() };
|
||||
let tet =
|
||||
SurfaceMesh { positions: p.clone(), triangles: t.clone(), tri_face: vec![0, 1, 1, 1], weld_distance: 0.0, ..SurfaceMesh::default() };
|
||||
let open = SurfaceMesh { positions: p, triangles: t[..3].to_vec(), tri_face: vec![0, 1, 1], weld_distance: 0.0, ..SurfaceMesh::default() };
|
||||
for mesh in [tet, open] {
|
||||
s.surfaces.push(Arc::new(TaggedSurface { name: "Tet".into(), mesh, faces: 2 }));
|
||||
@@ -956,7 +957,8 @@ mod tests {
|
||||
|
||||
let p = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
||||
let t = vec![[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]];
|
||||
let tet = SurfaceMesh { positions: p.clone(), triangles: t.clone(), tri_face: vec![0, 1, 1, 1], weld_distance: 0.0, ..SurfaceMesh::default() };
|
||||
let tet =
|
||||
SurfaceMesh { positions: p.clone(), triangles: t.clone(), tri_face: vec![0, 1, 1, 1], weld_distance: 0.0, ..SurfaceMesh::default() };
|
||||
let open = SurfaceMesh { positions: p, triangles: t[..3].to_vec(), tri_face: vec![0, 1, 1], weld_distance: 0.0, ..SurfaceMesh::default() };
|
||||
let mut s = Session::default();
|
||||
for mesh in [tet, open] {
|
||||
|
||||
@@ -75,6 +75,17 @@ pub struct SurfaceTri {
|
||||
pub face: u32,
|
||||
}
|
||||
|
||||
/// A segment of a geometry surface's B-rep edge (a named design edge), for picking and marking.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct SurfaceCurve {
|
||||
pub a: Vec3,
|
||||
pub b: Vec3,
|
||||
/// The surface (body) it belongs to, as [`SurfaceTri::body`].
|
||||
pub body: u32,
|
||||
/// The edge's index in that body's tessellation.
|
||||
pub edge: u32,
|
||||
}
|
||||
|
||||
/// Display data for a model and its geometry.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct DisplayMesh {
|
||||
@@ -90,6 +101,8 @@ pub struct DisplayMesh {
|
||||
pub surface_tris: Vec<SurfaceTri>,
|
||||
/// Geometry edges: where two faces meet, and open boundaries.
|
||||
pub surface_edges: Vec<[Vec3; 2]>,
|
||||
/// The B-rep edges of the geometry surfaces that have them, segment by segment.
|
||||
pub curves: Vec<SurfaceCurve>,
|
||||
/// Geometry surfaces (bodies) added.
|
||||
pub bodies: u32,
|
||||
pub bounds: Aabb3,
|
||||
@@ -267,6 +280,21 @@ impl DisplayMesh {
|
||||
self.bodies = self.bodies.saturating_add(1);
|
||||
}
|
||||
|
||||
/// Add the B-rep edges (polylines, by edge index) of the surface [`DisplayMesh::add_surface`]
|
||||
/// adds next, so they are picked as that body's edges; non-finite points are skipped.
|
||||
pub fn add_curves(&mut self, edges: &[Vec<[f64; 3]>]) {
|
||||
let body = self.bodies;
|
||||
for (e, points) in edges.iter().enumerate() {
|
||||
let Ok(edge) = u32::try_from(e) else { break };
|
||||
for w in points.windows(2) {
|
||||
let (a, b) = (Vec3::from(w[0]), Vec3::from(w[1]));
|
||||
if a.is_finite() && b.is_finite() {
|
||||
self.curves.push(SurfaceCurve { a, b, body, edge });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour of a geometry triangle: `single` when there is one body, else body `k`'s
|
||||
/// colour from `palette` (cycling), so the bodies of a design, such as the cells of a
|
||||
/// partitioned part, tell apart.
|
||||
@@ -415,7 +443,10 @@ mod tests {
|
||||
// triangle, which are skipped.
|
||||
let p = [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.]];
|
||||
let mut d = DisplayMesh::default();
|
||||
// Its B-rep edges: the diagonal (two segments) and a side; a non-finite point is skipped.
|
||||
d.add_curves(&[vec![p[0], [0.5, 0.5, 0.0], p[2]], vec![p[0], p[1]], vec![[f64::NAN; 3], p[1]]]);
|
||||
d.add_surface(&p, &[[0, 1, 2], [0, 2, 3], [0, 0, 1], [0, 1, 9]], &[0, 1, 1, 1]);
|
||||
assert_eq!(d.curves.iter().map(|c| (c.body, c.edge)).collect::<Vec<_>>(), vec![(0, 0), (0, 0), (0, 1)]);
|
||||
assert_eq!(d.surface_tris.len(), 2);
|
||||
// Four open sides and the diagonal between the two faces.
|
||||
assert_eq!(d.surface_edges.len(), 5);
|
||||
|
||||
@@ -16,7 +16,7 @@ mod raster;
|
||||
mod vec;
|
||||
|
||||
pub use camera::{Camera, Mat4, Orientation, StandardView};
|
||||
pub use display::{DisplayEdge, DisplayLine, DisplayMesh, DisplayNode, DisplayTri, SurfaceTri};
|
||||
pub use display::{DisplayEdge, DisplayLine, DisplayMesh, DisplayNode, DisplayTri, SurfaceCurve, SurfaceTri};
|
||||
pub use raster::{
|
||||
Canvas, LABEL_OFFSET, MAX_DIM, MAX_PIXELS, MAX_PRIMITIVES, RenderError, Rgb, Scene, SceneLabel, SceneLine, ScenePoint, SceneTri, fit_size, png,
|
||||
render, render_layers, render_layers_png, render_png, shade,
|
||||
|
||||
@@ -743,7 +743,11 @@ impl ModelApp {
|
||||
.filter_map(|body| {
|
||||
let tolerance = body.measure().map(|m| display_tolerance(&Aabb3 { min: m.bbox_min.into(), max: m.bbox_max.into() }));
|
||||
let mesh = tolerance.and_then(|t| body.tessellate(t)).map_err(|e| failed.push(format!("{}: {e}", body.name))).ok()?;
|
||||
let names = selection::BodyNames { name: body.name.clone(), faces: body.faces().iter().map(|f| f.name.clone()).collect() };
|
||||
let names = selection::BodyNames {
|
||||
name: body.name.clone(),
|
||||
faces: body.faces().iter().map(|f| f.name.clone()).collect(),
|
||||
edges: body.edge_names(&mesh),
|
||||
};
|
||||
Some((names, mesh))
|
||||
})
|
||||
.collect();
|
||||
@@ -754,13 +758,18 @@ impl ModelApp {
|
||||
}
|
||||
self.pick.names.clear();
|
||||
for (names, mesh) in &self.bodies.1 {
|
||||
d.add_curves(&mesh.edges);
|
||||
d.add_surface(&mesh.positions, &mesh.triangles, &mesh.tri_face);
|
||||
self.pick.names.push(names.clone());
|
||||
}
|
||||
// An imported surface's faces are named by their tags.
|
||||
for s in &self.session.surfaces {
|
||||
d.add_surface(&s.mesh.positions, &s.mesh.triangles, &s.mesh.tri_face);
|
||||
self.pick.names.push(selection::BodyNames { name: s.name.clone(), faces: (0..s.faces).map(|f| f.to_string()).collect() });
|
||||
self.pick.names.push(selection::BodyNames {
|
||||
name: s.name.clone(),
|
||||
faces: (0..s.faces).map(|f| f.to_string()).collect(),
|
||||
edges: Vec::new(),
|
||||
});
|
||||
}
|
||||
let was_empty = self.display.1.bounds.is_empty();
|
||||
self.display = (Some(self.revision), d);
|
||||
@@ -876,6 +885,7 @@ impl ModelApp {
|
||||
index: &index,
|
||||
elements: show_mesh,
|
||||
geometry: show_geometry && !mesh_faces,
|
||||
curves: show_geometry,
|
||||
};
|
||||
f(&picker)
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
//! 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. Edges and vertices have
|
||||
//! names only on a mesh that carries them (block and planar meshes); a design's edges have no
|
||||
//! persistent names yet.
|
||||
//! 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};
|
||||
|
||||
@@ -262,11 +263,13 @@ pub fn items_from_params(p: &Value) -> Result<Vec<Item>, String> {
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// A drawn geometry surface's names: its body and, by face tag, its faces.
|
||||
/// 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)]
|
||||
pub struct BodyNames {
|
||||
pub name: String,
|
||||
pub faces: Vec<String>,
|
||||
pub edges: Vec<String>,
|
||||
}
|
||||
|
||||
impl BodyNames {
|
||||
@@ -274,6 +277,11 @@ impl BodyNames {
|
||||
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 edge `edge` of this body, when it has a name.
|
||||
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
|
||||
@@ -337,6 +345,16 @@ pub struct Picker<'a> {
|
||||
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.
|
||||
@@ -385,18 +403,42 @@ impl Picker<'_> {
|
||||
|
||||
/// 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() + self.slack())
|
||||
self.surface(o, d).is_none_or(|s| along <= s.t() + slack)
|
||||
}
|
||||
|
||||
/// The candidate nearest on screen to `at` within `radius` that is visible: candidates are
|
||||
/// (index, screen distance, the point to check); the first visible of the nearest few wins.
|
||||
fn nearest_visible(&self, proj: &Projector, mut near: Vec<(usize, f64, Vec3)>, radius: f64) -> 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| self.visible(proj, c.2)).map(|c| c.0)
|
||||
/// 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.
|
||||
@@ -467,6 +509,11 @@ impl Picker<'_> {
|
||||
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
|
||||
@@ -611,6 +658,23 @@ impl Picker<'_> {
|
||||
}
|
||||
}
|
||||
outline(s, first, color);
|
||||
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.nodes[0], e.nodes[1])).copied()) {
|
||||
@@ -845,7 +909,7 @@ mod tests {
|
||||
Scene3 { model, display, index }
|
||||
}
|
||||
fn picker(&self) -> Picker<'_> {
|
||||
Picker { display: &self.display, names: &[], model: &self.model, index: &self.index, elements: true, geometry: false }
|
||||
Picker { display: &self.display, names: &[], model: &self.model, index: &self.index, elements: true, geometry: false, curves: false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -937,11 +1001,13 @@ mod tests {
|
||||
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()] }];
|
||||
let names = [BodyNames { name: "Plate".into(), faces: vec!["F1:a".into(), "F1:b".into()], edges: vec!["F1:a|F1:b".into()] }];
|
||||
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 };
|
||||
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);
|
||||
@@ -957,6 +1023,17 @@ mod tests {
|
||||
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]
|
||||
|
||||
Reference in new issue
Block a user