From a0f48c72074081fbddfd30553030432fe2e4c906 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:57:24 -0400 Subject: [PATCH] render: quadratic elements draw through their mid-side nodes; geometry edges over the mesh follow it Zoomed in on the hex20 crank arm, the element edges and the shaded faces did not meet (#260). The mesh is conforming; the display drew each hex20 face and edge as straight chords between corners while the mid-side nodes lie on the curved surface (up to 0.44 mm off the chord at 3 mm), and drew the geometry's face boundaries from the design tessellation, off both. Now a quadratic face is cut into triangles whose vertices are its corners and mid-side nodes, each edge is drawn as its two halves, and over the mesh the geometry's edges (and a hovered design edge) are drawn as the element edges on geometry edges (`DisplayEdge::feature`), so faces, edges, feature lines and the highlight share their vertices. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo --- crates/engine/src/view.rs | 10 +- crates/render/src/display.rs | 241 +++++++++++++++++++++++--------- crates/ui-egui/src/scene.rs | 47 +++++-- crates/ui-egui/src/selection.rs | 13 +- 4 files changed, 233 insertions(+), 78 deletions(-) diff --git a/crates/engine/src/view.rs b/crates/engine/src/view.rs index 823b51d..890a361 100644 --- a/crates/engine/src/view.rs +++ b/crates/engine/src/view.rs @@ -141,8 +141,14 @@ fn model_scene(d: &DisplayMesh, p: &Palette) -> Scene { s.tris.extend(d.surface_tris.iter().map(|t| SceneTri { p: t.p, normal: t.normal, color: d.body_color(t, p.geometry, &p.bodies) })); } s.lines = d.edges.iter().map(|e| SceneLine { a: e.p[0], b: e.p[1], color: p.mesh_edge, width: EDGE_WIDTH }).collect(); - let edge = if faces_drawn { p.mesh_edge } else { p.geometry_edge }; - s.lines.extend(d.surface_edges.iter().map(|[a, b]| SceneLine { a: *a, b: *b, color: edge, width: GEOMETRY_EDGE_WIDTH })); + if faces_drawn { + s.lines.extend(d.surface_edges.iter().map(|[a, b]| SceneLine { a: *a, b: *b, color: p.mesh_edge, width: GEOMETRY_EDGE_WIDTH })); + } else { + // Over the mesh the geometry's edges are drawn along it (the element edges on them), so + // they lie on the element faces. + let edge = p.geometry_edge; + s.lines.extend(d.edges.iter().filter(|e| e.feature).map(|e| SceneLine { a: e.p[0], b: e.p[1], color: edge, width: GEOMETRY_EDGE_WIDTH })); + } s.lines.extend(d.lines.iter().map(|l| SceneLine { a: l.a, b: l.b, color: p.element(l.kind), width: LINE_ELEMENT_WIDTH })); s } diff --git a/crates/render/src/display.rs b/crates/render/src/display.rs index a1c0a66..3f903c1 100644 --- a/crates/render/src/display.rs +++ b/crates/render/src/display.rs @@ -3,9 +3,12 @@ //! Finite elements: the faces nobody shares (the skin of the solid elements, and every shell), //! the element edges on those faces, line elements and nodes. Faces shared by two solid elements //! are inside the mesh and are left out, so a model of a million elements draws its surface, not -//! its interior. Higher-order elements (tet10, hex20, tri6, quad8) draw through their corner -//! nodes. Every element primitive keeps its node labels, so results (displacements, contours) -//! can be drawn on it ([`DisplayMesh::displaced`]). +//! its interior. Higher-order elements (tet10, hex20, tri6, quad8) draw through all their nodes: +//! a face is cut into triangles whose vertices are its corners and mid-side nodes, and each +//! edge is drawn as its two halves through its mid-side node, so the faces, the edges, the +//! feature lines and a highlight all share the same vertices and meet exactly at any zoom. +//! Every element primitive keeps its node labels, so results (displacements, contours) can be +//! drawn on it ([`DisplayMesh::displaced`]). //! //! Geometry: triangulated boundary surfaces (tessellated bodies, imported surfaces) added with //! [`DisplayMesh::add_surface`], drawn with the edges between their faces. @@ -40,11 +43,16 @@ impl DisplayTri { } } -/// An element edge. +/// A drawn segment of an element edge: the whole edge of a linear element, a half (corner to +/// mid-side node) of a quadratic one, so edges bend with the faces they bound. #[derive(Clone, Copy, Debug, PartialEq)] pub struct DisplayEdge { pub p: [Vec3; 2], pub nodes: [u32; 2], + /// The corner nodes of the element edge it is (part of), lower first. + pub corners: [u32; 2], + /// The element edge lies on a geometry edge: a feature line of the mesh. + pub feature: bool, } /// A line element. @@ -108,29 +116,43 @@ pub struct DisplayMesh { pub bounds: Aabb3, } -/// The faces of an element as corner indices (0-based into its node list), each wound -/// counter-clockwise seen from outside for a positively oriented element. A solid's faces come in -/// the order [`ElementKind::faces`] numbers them (with the same corners); a shell draws its top -/// side, face 1. -fn faces(kind: ElementKind) -> &'static [&'static [usize]] { - match kind { - ElementKind::Rod2 | ElementKind::Rod3 | ElementKind::Beam2 => &[], - ElementKind::Tri3 | ElementKind::Tri6 => &[&[0, 1, 2]], - ElementKind::Quad4 | ElementKind::Quad8 => &[&[0, 1, 2, 3]], - ElementKind::Tet4 | ElementKind::Tet10 => &[&[0, 2, 1], &[0, 1, 3], &[1, 2, 3], &[2, 0, 3]], - ElementKind::Wedge6 => &[&[0, 2, 1], &[3, 4, 5], &[0, 1, 4, 3], &[1, 2, 5, 4], &[2, 0, 3, 5]], - ElementKind::Hex8 | ElementKind::Hex20 => &[&[0, 3, 2, 1], &[4, 5, 6, 7], &[0, 1, 5, 4], &[1, 2, 6, 5], &[2, 3, 7, 6], &[3, 0, 4, 7]], - } +/// The faces drawn of an element, as positions in its node list ([`ElementKind::faces`]: the +/// corners counter-clockwise seen from outside, then the mid-side nodes of quadratic kinds): a +/// solid's faces in their numbering, a shell's top side (face 1). +fn drawn_faces(kind: ElementKind) -> &'static [&'static [usize]] { + let all = kind.faces(); + if kind.dimension() == 2 { all.get(..1).unwrap_or(&[]) } else { all } } -/// Most corners of any element (hex). -const MAX_CORNERS: usize = 8; +/// The corners of a face listed with `n` nodes: a quadratic face (tri6, quad8) has as many +/// mid-side nodes as corners. +fn face_corner_count(n: usize) -> usize { + if n >= 6 { n / 2 } else { n } +} -/// A face's identity regardless of winding and starting corner: its sorted node labels, a +/// Triangles (positions in the face's boundary ring) that fill a face whose boundary ring has +/// `n` points. A quadratic face's ring alternates corner and mid-side node, and every triangle's +/// vertices are nodes: one triangle cut off at each corner, and the polygon of the mid-side +/// nodes fanned, so the drawn face passes through every node and its sides are the drawn edges. +fn face_triangles(n: usize, quadratic: bool) -> Vec<[usize; 3]> { + if !quadratic { + return (1..n.saturating_sub(1)).map(|k| [0, k, k + 1]).collect(); + } + let mut out: Vec<[usize; 3]> = (0..n).step_by(2).map(|c| [c, c + 1, (c + n - 1) % n]).collect(); + let mids: Vec = (1..n).step_by(2).collect(); + for k in 1..mids.len().saturating_sub(1) { + if let (Some(&a), Some(&b), Some(&c)) = (mids.first(), mids.get(k), mids.get(k + 1)) { + out.push([a, b, c]); + } + } + out +} + +/// A face's identity regardless of winding and starting corner: its sorted corner labels, a /// triangle padded with `u32::MAX`. fn face_key(e: &[u32], face: &[usize]) -> Option<[u32; 4]> { let mut k = [u32::MAX; 4]; - for (slot, &i) in k.iter_mut().zip(face) { + for (slot, &i) in k.iter_mut().zip(face.iter().take(face_corner_count(face.len()))) { *slot = *e.get(i)?; } k.sort_unstable(); @@ -166,65 +188,85 @@ impl DisplayMesh { .elements() .values() .filter(|e| e.kind.dimension() == 3) - .flat_map(|e| faces(e.kind).iter().filter_map(|f| face_key(&e.nodes, f))) + .flat_map(|e| drawn_faces(e.kind).iter().filter_map(|f| face_key(&e.nodes, f))) .collect(); keys.sort_unstable(); let mut inner: Vec<[u32; 4]> = keys.windows(2).filter(|w| w.first() == w.get(1)).filter_map(|w| w.first().copied()).collect(); inner.dedup(); drop(keys); - let mut edges: Vec<(u32, u32)> = Vec::new(); - for (&label, e) in model.elements() { - // Corners: one past the highest face index (line elements: their two ends). - let corners = faces(e.kind).iter().flat_map(|f| f.iter()).max().map_or(2, |m| m + 1).min(MAX_CORNERS); - let mut at = [Vec3::ZERO; MAX_CORNERS]; - let mut found = 0; - for (slot, &n) in at.iter_mut().zip(e.nodes.iter().take(corners)) { - if let Some(p) = pos(n) { - *slot = p; - found += 1; + // Element edges on geometry edges (by their corner nodes): the mesh's feature lines. + let classification = model.classification(); + let mut features: Vec<(u32, u32)> = Vec::new(); + for (&element, tags) in classification.elements() { + let Some(e) = model.element(element) else { continue }; + for (&edge, &tag) in &tags.edges { + if classification.entity(tag).is_some_and(|g| g.entity.dimension() == 1) + && let Some(local) = e.kind.edges().get(usize::from(edge).wrapping_sub(1)) + && let (Some(&a), Some(&b)) = (local.first().and_then(|&i| e.nodes.get(i)), local.get(1).and_then(|&i| e.nodes.get(i))) + { + features.push((a.min(b), a.max(b))); } } - if found < corners { - continue; - } - let at = at.get(..corners).unwrap_or(&[]); + } + features.sort_unstable(); + features.dedup(); + // (segment's nodes, the element edge's corners), each sorted. + let mut edges: Vec<((u32, u32), (u32, u32))> = Vec::new(); + for (&label, e) in model.elements() { if e.kind.dimension() == 1 { - if let (Some(&a), Some(&b), Some(&na), Some(&nb)) = (at.first(), at.get(1), e.nodes.first(), e.nodes.get(1)) { + if let (Some(&na), Some(&nb)) = (e.nodes.first(), e.nodes.get(1)) + && let (Some(a), Some(b)) = (pos(na), pos(nb)) + { out.lines.push(DisplayLine { a, b, nodes: [na, nb], kind: e.kind, element: label }); } continue; } + let Some(at) = e.nodes.iter().map(|&n| pos(n)).collect::>>() else { continue }; + if at.len() != e.kind.node_count() { + continue; + } let solid = e.kind.dimension() == 3; - let element_centre = centre(at); - for (number, f) in faces(e.kind).iter().enumerate() { + let element_centre = centre(at.get(..e.kind.corner_count()).unwrap_or(&at)); + for (number, f) in drawn_faces(e.kind).iter().enumerate() { let face = u8::try_from(number + 1).unwrap_or(u8::MAX); if solid && face_key(&e.nodes, f).is_some_and(|k| inner.binary_search(&k).is_ok()) { continue; } - let p: Vec = f.iter().filter_map(|&i| at.get(i).copied()).collect(); - let labels: Vec = f.iter().filter_map(|&i| e.nodes.get(i).copied()).collect(); + // The face's boundary: corner, mid-side node, corner, … (corners only when linear). + let corners = face_corner_count(f.len()); + let ring: Vec = (0..corners).flat_map(|j| std::iter::once(j).chain((f.len() > corners).then_some(corners + j))).collect(); + let Some(ring) = + ring.iter().map(|&k| f.get(k).and_then(|&i| Some((*at.get(i)?, *e.nodes.get(i)?)))).collect::>>() + else { + continue; + }; + let p: Vec = ring.iter().map(|r| r.0).collect(); let Some(mut normal) = polygon_normal(&p) else { continue }; if solid && normal.dot(centre(&p) - element_centre) < 0.0 { normal = -normal; } - for k in 1..p.len().saturating_sub(1) { - if let (Some(&a), Some(&b), Some(&c), Some(&la), Some(&lb), Some(&lc)) = - (p.first(), p.get(k), p.get(k + 1), labels.first(), labels.get(k), labels.get(k + 1)) - { - out.tris.push(DisplayTri { p: [a, b, c], nodes: [la, lb, lc], normal, kind: e.kind, element: label, face }); + for [a, b, c] in face_triangles(ring.len(), corners < ring.len()) { + if let (Some(&(pa, la)), Some(&(pb, lb)), Some(&(pc, lc))) = (ring.get(a), ring.get(b), ring.get(c)) { + out.tris.push(DisplayTri { p: [pa, pb, pc], nodes: [la, lb, lc], normal, kind: e.kind, element: label, face }); } } - for (j, &i) in f.iter().enumerate() { - let i2 = f.get((j + 1) % f.len()).copied().unwrap_or(i); - if let (Some(&a), Some(&b)) = (e.nodes.get(i), e.nodes.get(i2)) { - edges.push((a.min(b), a.max(b))); - } + let step = ring.len() / corners.max(1); + for (k, &(_, a)) in ring.iter().enumerate() { + let b = ring.get((k + 1) % ring.len()).map_or(a, |r| r.1); + let (c0, c1) = (ring.get(k - k % step).map_or(a, |r| r.1), ring.get((k - k % step + step) % ring.len()).map_or(b, |r| r.1)); + edges.push(((a.min(b), a.max(b)), (c0.min(c1), c0.max(c1)))); } } } edges.sort_unstable(); edges.dedup(); - out.edges = edges.into_iter().filter_map(|(a, b)| Some(DisplayEdge { p: [pos(a)?, pos(b)?], nodes: [a, b] })).collect(); + out.edges = edges + .into_iter() + .filter_map(|((a, b), corners)| { + let feature = features.binary_search(&corners).is_ok(); + Some(DisplayEdge { p: [pos(a)?, pos(b)?], nodes: [a, b], corners: [corners.0, corners.1], feature }) + }) + .collect(); out } @@ -351,7 +393,7 @@ impl DisplayMesh { .collect(); let mut out = DisplayMesh { tris, - edges: self.edges.iter().map(|e| DisplayEdge { p: [at(e.p[0], e.nodes[0]), at(e.p[1], e.nodes[1])], nodes: e.nodes }).collect(), + edges: self.edges.iter().map(|e| DisplayEdge { p: [at(e.p[0], e.nodes[0]), at(e.p[1], e.nodes[1])], ..*e }).collect(), lines: self.lines.iter().map(|l| DisplayLine { a: at(l.a, l.nodes[0]), b: at(l.b, l.nodes[1]), ..*l }).collect(), nodes: self.nodes.iter().map(|n| DisplayNode { label: n.label, p: at(n.p, n.label) }).collect(), ..Default::default() @@ -509,17 +551,6 @@ mod tests { /// the element face a set or load refers to. #[test] fn drawn_faces_carry_the_models_face_numbers() { - for kind in [ElementKind::Tet4, ElementKind::Tet10, ElementKind::Wedge6, ElementKind::Hex8, ElementKind::Hex20] { - let drawn = faces(kind); - assert_eq!(drawn.len(), kind.faces().len(), "{kind:?}"); - for (d, m) in drawn.iter().zip(kind.faces()) { - let corners = d.len(); - let (mut a, mut b) = (d.to_vec(), m.get(..corners).unwrap().to_vec()); - a.sort_unstable(); - b.sort_unstable(); - assert_eq!(a, b, "{kind:?}"); - } - } let m = block(2, 1, 1); let d = DisplayMesh::from_model(&m); for t in &d.tris { @@ -532,6 +563,88 @@ mod tests { assert_eq!((t.element, t.face, h.t), (2, 4, 8.0)); } + /// `block(2, 1, 1)` as hex20: a mid-side node on every edge, the ones on the top face + /// raised by 0.1 (a curved top, as a mesher puts them on a curved surface), and element 1's + /// edge 5 (corners 5 → 6, along the top) on a geometry edge. + fn curved_hex20_block() -> Model { + let linear = block(2, 1, 1); + let mut m = Model::default(); + for (&l, n) in linear.nodes() { + m.set_node(l, n.xyz).unwrap(); + } + let mut mids = std::collections::BTreeMap::new(); + let mut next = 100; + for (&label, e) in linear.elements() { + let mut nodes = e.nodes.clone(); + for edge in ElementKind::Hex20.edges() { + let (a, b) = (e.nodes[edge[0]], e.nodes[edge[1]]); + let id = *mids.entry((a.min(b), a.max(b))).or_insert_with(|| { + let (p, q) = (linear.node(a).unwrap().xyz, linear.node(b).unwrap().xyz); + let mut mid = [0.5 * (p[0] + q[0]), 0.5 * (p[1] + q[1]), 0.5 * (p[2] + q[2])]; + if p[2] == 1.0 && q[2] == 1.0 { + mid[2] += 0.1; + } + next += 1; + m.set_node(next, mid).unwrap(); + next + }); + nodes.push(id); + } + m.set_element(label, ElementKind::Hex20, nodes).unwrap(); + } + let edge = fieldcraft_model::GeometryRef::new("B", fieldcraft_model::Entity::Edge("top".into())).unwrap(); + let tag = m.geometry_tag(&edge).unwrap(); + m.tag_element_edge(1, 5, tag).unwrap(); + m + } + + /// A curved hex20 face is drawn through its mid-side nodes, and its faces, edges and feature + /// lines share their vertices exactly (#260: lines met the faces nowhere when zoomed in). + #[test] + fn quadratic_faces_and_edges_share_their_vertices() { + let m = curved_hex20_block(); + let d = DisplayMesh::from_model(&m); + let at = |l: u32| Vec3::from(m.node(l).unwrap().xyz); + // Every vertex drawn is a node, exactly where the node is. + for t in &d.tris { + assert!(t.nodes.iter().zip(t.p).all(|(&l, p)| at(l) == p), "{t:?}"); + } + for e in &d.edges { + assert!(e.nodes.iter().zip(e.p).all(|(&l, p)| at(l) == p), "{e:?}"); + } + // 10 skin faces, 6 triangles each; the 20 element edges in two halves each. + assert_eq!(d.tris.len(), 60); + assert_eq!(d.edges.len(), 40); + // The drawn edges are exactly the sides of the faces' outlines. + let mut sides: Vec<((u32, u8), [u32; 2])> = d + .tris + .iter() + .flat_map(|t| { + [0, 1, 2].map(|k| { + ((t.element, t.face), { + let mut s = [t.nodes[k], t.nodes[(k + 1) % 3]]; + s.sort_unstable(); + s + }) + }) + }) + .collect(); + sides.sort_unstable(); + let mut outline: Vec<[u32; 2]> = sides.chunk_by(|a, b| a == b).filter(|r| r.len() == 1).map(|r| r[0].1).collect(); + outline.sort_unstable(); + outline.dedup(); + let mut drawn: Vec<[u32; 2]> = d.edges.iter().map(|e| e.nodes).collect(); + drawn.sort_unstable(); + assert_eq!(outline, drawn); + // A top edge bows up through its mid-side node. + assert!(d.edges.iter().any(|e| e.p.iter().any(|p| (p.z - 1.1).abs() < 1e-12))); + // The edge on the geometry edge is a feature line, both halves; the rest are not. + let feature: Vec<[u32; 2]> = d.edges.iter().filter(|e| e.feature).map(|e| e.corners).collect(); + let e1 = &m.element(1).unwrap().nodes; + let top = [e1[4].min(e1[5]), e1[4].max(e1[5])]; + assert_eq!(feature, vec![top, top]); + } + #[test] fn empty_and_deterministic() { assert_eq!(DisplayMesh::from_model(&Model::default()), DisplayMesh::default()); diff --git a/crates/ui-egui/src/scene.rs b/crates/ui-egui/src/scene.rs index dd68686..37ced38 100644 --- a/crates/ui-egui/src/scene.rs +++ b/crates/ui-egui/src/scene.rs @@ -2,8 +2,9 @@ //! colours. The GPU viewport and the headless render (`ui.render`) draw the same [`Scene`]. //! //! Geometry (the open design, imported surfaces) draws as faces with their boundaries while -//! the model has no element faces; once it is meshed, as boundaries only, so the mesh is not -//! hidden behind coincident faces. The analysis setup's symbols ([`symbols`]) go over the model, +//! the model has no element faces; once it is meshed, as its boundaries drawn along the mesh +//! (the element edges on geometry edges), so the mesh is not hidden behind coincident faces and +//! every line lies on the element faces. The analysis setup's symbols ([`symbols`]) go over the model, //! muted over a results plot. use fieldcraft_render::glyphs::{GlyphShow, Glyphs}; @@ -97,14 +98,16 @@ pub fn build(display: &DisplayMesh, style: Style, show: Show, t: &Tokens) -> Sce let color = rgb(if style == Style::Wireframe { t.hex } else { t.mesh_edge }); s.lines = display.edges.iter().map(|e| SceneLine { a: e.p[0], b: e.p[1], color, width: EDGE_WIDTH }).collect(); } - // Face boundaries in every style: they are the geometry's outline. + // Face boundaries in every style: they are the geometry's outline. Over the mesh they are + // the element edges on geometry edges, so they lie exactly on the element faces; the + // geometry's own tessellation lies off them by its chord error. if show.geometry { - s.lines.extend(display.surface_edges.iter().map(|[a, b]| SceneLine { - a: *a, - b: *b, - color: rgb(t.geometry_edge), - width: GEOMETRY_EDGE_WIDTH, - })); + let color = rgb(t.geometry_edge); + if mesh_faces { + s.lines.extend(display.edges.iter().filter(|e| e.feature).map(|e| SceneLine { a: e.p[0], b: e.p[1], color, width: GEOMETRY_EDGE_WIDTH })); + } else { + s.lines.extend(display.surface_edges.iter().map(|[a, b]| SceneLine { a: *a, b: *b, color, width: GEOMETRY_EDGE_WIDTH })); + } } if show.mesh { s.lines.extend(display.lines.iter().map(|l| SceneLine { a: l.a, b: l.b, color: rgb(t.element(l.kind)), width: LINE_ELEMENT_WIDTH })); @@ -181,6 +184,32 @@ mod tests { assert!(s.points.iter().all(|p| p.p.y == 0.0), "only the seeded edge has ticks"); } + /// Over the mesh the geometry's outline is drawn along the element edges on geometry edges, + /// never the design's tessellation, which lies off the element faces (#260). + #[test] + fn geometry_edges_over_the_mesh_follow_its_element_edges() { + let mut m = Model::default(); + for (i, p) in + [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.], [0., 0., 1.], [1., 0., 1.], [1., 1., 1.], [0., 1., 1.]].into_iter().enumerate() + { + m.set_node(i as u32 + 1, p).unwrap(); + } + m.set_element(1, ElementKind::Hex8, (1..=8).collect()).unwrap(); + let edge = fieldcraft_model::GeometryRef::new("B", fieldcraft_model::Entity::Edge("e".into())).unwrap(); + let tag = m.geometry_tag(&edge).unwrap(); + m.tag_element_edge(1, 1, tag).unwrap(); + let mut d = DisplayMesh::from_model(&m); + // The design's own edge, a little off the element edge (its chord error). + d.add_surface(&[[0.0, -0.01, 0.0], [1.0, -0.01, 0.0], [0.5, -0.01, 1.0]], &[[0, 1, 2]], &[0]); + let t = Tokens::dark(); + let geometry = |s: &Scene| s.lines.iter().filter(|l| l.color == rgb(t.geometry_edge)).map(|l| [l.a, l.b]).collect::>(); + let s = build(&d, Style::ShadedEdges, Show::DEFAULT, &t); + assert_eq!(geometry(&s), vec![[Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)]]); + // Without the mesh the design's edges are drawn. + let s = build(&d, Style::ShadedEdges, Show { mesh: false, ..Show::DEFAULT }, &t); + assert_eq!(geometry(&s).len(), 3); + } + #[test] fn styles_choose_faces_edges_and_nodes() { let mut m = Model::default(); diff --git a/crates/ui-egui/src/selection.rs b/crates/ui-egui/src/selection.rs index 1ffa9dc..0d1425a 100644 --- a/crates/ui-egui/src/selection.rs +++ b/crates/ui-egui/src/selection.rs @@ -389,6 +389,11 @@ impl Picker<'_> { } } + /// Element faces are drawn: the geometry is drawn along the mesh. + fn elements_drawn(&self) -> bool { + self.elements && !self.display.tris.is_empty() + } + /// The drawn faces' triangles (for a depth map). fn drawn(&self) -> impl Iterator + '_ { let e = self.display.tris.iter().filter(|_| self.elements).map(|t| t.p); @@ -519,7 +524,7 @@ impl Picker<'_> { .display .edges .iter() - .filter_map(|e| Some((e.p[0], e.p[1], *self.index.edge_tag.get(&edge_key(e.nodes[0], e.nodes[1]))?))) + .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, _))| { @@ -658,7 +663,9 @@ impl Picker<'_> { } } outline(s, first, color); - if m.edges && self.curves { + // Over element faces a design edge is marked along the mesh's edges on it (below), which + // lie on the faces; its own polyline lies off them by the tessellation's chord error. + if m.edges && self.curves && !self.elements_drawn() { // Which edges of each drawn body are marked, worked out once per edge. let marked: Vec> = self .names @@ -677,7 +684,7 @@ impl Picker<'_> { } 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()) { + 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 }); } }