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 <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-14 22:13:15 -04:00
1 parent 9e38fe866d
commit a0f48c7207
4 files changed
+233 -78

No files matched your search

+38 -9
View File
@@ -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::<Vec<_>>();
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();
+10 -3
View File
@@ -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<Item = [Vec3; 3]> + '_ {
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<Vec<bool>> = 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 });
}
}