mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 12:01:10 +00:00
Display meshes: tests count the planner's grids; a mesh's outline is one line per straight edge
The display mesh is the tessellation planner's (8b6bfc48c: the mesher plans a schema 4
record), which grids a planar face with no side longer than a fifth of the body's extent
(docs/capabilities.md, "Display meshes"); truck's mesher gave a box face two triangles. The
tests that counted truck's mesh now count the planner's, worked out by hand from that rule:
- a 10 x 20 x 30 box (37.4 mm diagonal): edges in 2, 3 and 5 pieces, faces of 2 x 3, 2 x 5
and 3 x 5 cells, 124 triangles; welded, the box lattice's 3 x 4 x 6 nodes less the
1 x 2 x 4 inside it, 64 positions (obj_export_is_welded_and_keeps_names);
- a 20 x 15 x 10 box: 4 x 3, 4 x 2 and 3 x 2 cells, 104 triangles (an_opacity_class_change...);
- a 20 x 20 x 10 box saved as a mesh: its edges in 4 and 2 pieces, 40 segments per box
(imported_mesh_edges_draw_in_each_style...);
- the cylinder's top cap is under a third of its triangles (select_other_over_a_dense_cluster...).
An imported mesh's outline was one line per crease side, so a box exported at that spacing
showed each edge in pieces: a click on a 10 mm edge measured 5 mm, or hit the node between
two pieces (measure_imported_stl_edges_and_vertices..., mesh_outline_hover_cache...). Outline
sides that run straight on through a vertex no other outline side meets are now one line, as
the CAD model's edge was.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -156,11 +156,55 @@ fn mesh_outlines(positions: &[Vec3], triangles: &[[u32; 3]]) -> Vec<Vec<Vec3>> {
|
||||
[(_, _, i), (_, _, j)] => normal(*i as usize).zip(normal(*j as usize)).is_some_and(|(n, m)| n.dot(m) <= cos),
|
||||
_ => true,
|
||||
};
|
||||
if visible && let (Some(&a), Some(&b)) = (positions.get(a as usize), positions.get(b as usize)) {
|
||||
edges.push(vec![a, b]);
|
||||
if visible && positions.get(a as usize).is_some() && positions.get(b as usize).is_some() {
|
||||
edges.push((a, b));
|
||||
}
|
||||
}
|
||||
edges
|
||||
straight_runs(positions, &edges)
|
||||
}
|
||||
|
||||
/// The outline sides `sides` (vertex index pairs) as polylines: sides that run on straight
|
||||
/// through a vertex no other outline side meets are one line. A mesh whose straight edges are
|
||||
/// in pieces (a box exported at a grid's spacing) then has one outline per edge, as its CAD
|
||||
/// model did, and a click on it measures the whole edge.
|
||||
fn straight_runs(positions: &[Vec3], sides: &[(u32, u32)]) -> Vec<Vec<Vec3>> {
|
||||
let mut at: std::collections::BTreeMap<u32, Vec<usize>> = std::collections::BTreeMap::new();
|
||||
for (i, &(a, b)) in sides.iter().enumerate() {
|
||||
at.entry(a).or_default().push(i);
|
||||
at.entry(b).or_default().push(i);
|
||||
}
|
||||
let point = |v: u32| positions.get(v as usize).copied().unwrap_or(Vec3::ZERO);
|
||||
let other = |i: usize, v: u32| sides.get(i).map_or(v, |&(a, b)| if a == v { b } else { a });
|
||||
// The side that continues `i` straight on through `v`, when `v` joins exactly two.
|
||||
let next = |i: usize, v: u32| -> Option<usize> {
|
||||
let [p, q] = at.get(&v)?.as_slice() else { return None };
|
||||
let j = if *p == i { *q } else { *p };
|
||||
let (into, out) = ((point(v) - point(other(i, v))).normalized()?, (point(other(j, v)) - point(v)).normalized()?);
|
||||
(into.dot(out) > 1.0 - 1e-9).then_some(j)
|
||||
};
|
||||
let mut used = vec![false; sides.len()];
|
||||
// Marks side `i` used; false when it already was.
|
||||
let mut take = |i: usize| used.get_mut(i).is_some_and(|u| !std::mem::replace(u, true));
|
||||
let mut runs = Vec::new();
|
||||
for (i, &(a, b)) in sides.iter().enumerate() {
|
||||
if !take(i) {
|
||||
continue;
|
||||
}
|
||||
let mut run = std::collections::VecDeque::from([a, b]);
|
||||
for (mut side, mut end, front) in [(i, b, false), (i, a, true)] {
|
||||
while let Some(j) = next(side, end).filter(|j| take(*j)) {
|
||||
end = other(j, end);
|
||||
side = j;
|
||||
if front {
|
||||
run.push_front(end);
|
||||
} else {
|
||||
run.push_back(end);
|
||||
}
|
||||
}
|
||||
}
|
||||
runs.push(run.into_iter().map(point).collect());
|
||||
}
|
||||
runs
|
||||
}
|
||||
|
||||
/// Only proper rotations and translations preserve triangle winding, normal lengths and the
|
||||
@@ -182,7 +226,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mesh_outlines_keep_boundaries_and_creases_without_smooth_facet_lines() {
|
||||
for (degrees, expected) in [(0.0f64, 4), (15.0, 4), (30.0, 5), (45.0, 5), (90.0, 5)] {
|
||||
// Flat (0 degrees), the sides from Y through the origin to -Y run straight on: one line.
|
||||
for (degrees, expected) in [(0.0f64, 3), (15.0, 4), (30.0, 5), (45.0, 5), (90.0, 5)] {
|
||||
let angle = degrees.to_radians();
|
||||
let p = [Vec3::ZERO, Vec3::X, Vec3::Y, Vec3::new(0., -angle.cos(), angle.sin())];
|
||||
let triangles = [[0, 1, 2], [1, 0, 3]];
|
||||
@@ -215,6 +260,12 @@ mod tests {
|
||||
let edges = mesh_outlines(&p, &triangles);
|
||||
assert_eq!(edges.len(), 2 * n as usize);
|
||||
assert!(edges.iter().all(|e| e[0].z == e[1].z));
|
||||
// A straight edge in pieces (a node at its middle, where the facets of the face meet)
|
||||
// is one outline; its corners stay corners.
|
||||
let p = [Vec3::ZERO, Vec3::X * 2., Vec3::new(2., 1., 0.), Vec3::Y, Vec3::X];
|
||||
let edges = mesh_outlines(&p, &[[0, 4, 3], [4, 1, 2], [4, 2, 3]]);
|
||||
assert_eq!(edges.len(), 4, "{edges:?}");
|
||||
assert!(edges.iter().any(|e| e == &[Vec3::ZERO, Vec3::X, Vec3::X * 2.] || e == &[Vec3::X * 2., Vec3::X, Vec3::ZERO]), "{edges:?}");
|
||||
// Bad indices and zero-area facets do not invent outlines or panic.
|
||||
assert!(mesh_outlines(&[Vec3::ZERO, Vec3::X, Vec3::X * 2.], &[[0, 1, 99], [0, 1, 2]]).is_empty());
|
||||
}
|
||||
|
||||
@@ -838,8 +838,12 @@ mod tests {
|
||||
assert!(o.contains("o Body1") && o.contains("\nf "));
|
||||
}
|
||||
|
||||
/// OBJ shares a face's vertices (a cube: 8 positions, 6 normals, not 36 of each) and keeps
|
||||
/// the body name as written, spaces included.
|
||||
/// OBJ shares the vertices faces meet at and keeps the body name as written, spaces included.
|
||||
/// The 10 × 20 × 30 box meshes as the planner's grids, no side longer than a fifth of its
|
||||
/// extent (the 37.4 mm diagonal): its edges in 2, 3 and 5 pieces, so its faces are grids of
|
||||
/// 2 × 3, 2 × 5 and 3 × 5 cells, 2 · (6 + 10 + 15) · 2 = 124 triangles. Welded, their
|
||||
/// corners are the 3 · 4 · 6 = 72 nodes of the box's lattice less the 1 · 2 · 4 inside it: 64
|
||||
/// positions (unwelded, 372), and one normal per face.
|
||||
#[test]
|
||||
fn obj_export_is_welded_and_keeps_names() {
|
||||
let (_, m) = model();
|
||||
@@ -850,10 +854,10 @@ mod tests {
|
||||
let o = String::from_utf8(export(&st, &[], Format::Obj, "t").unwrap()).unwrap();
|
||||
let count = |p: &str| o.lines().filter(|l| l.starts_with(p)).count();
|
||||
assert!(o.contains("\no Red Block\n"), "{o}");
|
||||
assert_eq!((count("v "), count("vn "), count("f ")), (8, 6, 12), "{o}");
|
||||
assert_eq!((count("v "), count("vn "), count("f ")), (64, 6, 124), "{o}");
|
||||
let ids: Vec<usize> =
|
||||
o.lines().filter(|l| l.starts_with("f ")).flat_map(|l| l.split([' ', '/']).skip(1)).filter_map(|x| x.parse().ok()).collect();
|
||||
assert!(ids.iter().all(|i| (1..=8).contains(i) || (1..=6).contains(i)));
|
||||
assert!(ids.iter().all(|i| (1..=64).contains(i)));
|
||||
}
|
||||
|
||||
/// The design properties land in each format's own metadata (#1015): STEP and IGES headers,
|
||||
|
||||
@@ -796,7 +796,9 @@ fn select_other_over_a_dense_cluster_looks_the_face_up_once() {
|
||||
// not the face's or the body's; every point on the face is still counted.
|
||||
let mut c = cylinder_with_cluster(json!({"plane": {"face": [0, 0, 20]}}), [5.0, 4.0], "top");
|
||||
let (many, body, face, near) = (c.many, c.body_tris, c.face_tris, c.near_tris);
|
||||
assert!(body >= 4 * face && 4 * near <= face, "{body} triangles in the body, {face} in the face, {near} of them near the points");
|
||||
// The top cap is one of the cylinder's three faces (the planner meshes both caps alike), under
|
||||
// a third of its triangles; the points lie on a small part of it.
|
||||
assert!(body >= 3 * face && 4 * near <= face, "{body} triangles in the body, {face} in the face, {near} of them near the points");
|
||||
let (list, (lookups, tested)) = c.select_other();
|
||||
assert_eq!((rows_named(&list, "Point"), rows_named(&list, "Face")), (crate::select_other::MAX_PER_KIND, 1), "{list}");
|
||||
assert_eq!(list["total"], json!(many + 1), "every point on the face is counted: {list}");
|
||||
|
||||
@@ -6999,7 +6999,10 @@ mod tests {
|
||||
for style in 0..4 {
|
||||
app.ui.visual_style = style;
|
||||
let scene = build_scene(&app).merged();
|
||||
let lines = if style == 1 { 0 } else { 24 * crate::gpu::LINE_SIZE };
|
||||
// Twelve edges per placed box, in the saved mesh's pieces: no side longer than a fifth
|
||||
// of the 30 mm diagonal, so the eight 20 mm edges in four and the four 10 mm ones in
|
||||
// two, 40 segments.
|
||||
let lines = if style == 1 { 0 } else { 2 * 40 * crate::gpu::LINE_SIZE };
|
||||
assert_eq!(scene.lines.len(), lines, "style {style}: twelve edges per placed box");
|
||||
assert_eq!(scene.hidden.len(), if style == 3 { lines } else { 0 });
|
||||
assert_eq!(scene.tris.is_empty(), style == 2, "wireframe draws only the edges");
|
||||
|
||||
@@ -219,7 +219,9 @@ fn an_opacity_class_change_keeps_vertices_and_rewrites_only_the_runs() {
|
||||
assert!(block.bytes.opaque.is_empty() && !block.bytes.glass.is_empty(), "the triangles moved to the glass runs");
|
||||
let triangles: u32 = block.bytes.glass.iter().map(|r| r.len() as u32).sum::<u32>() / 3;
|
||||
assert_eq!(block.bytes.glass.len(), 1, "one body of one class is one run, drawn with no index buffer");
|
||||
assert_eq!(triangles, 12, "every triangle of the box");
|
||||
// The planner's grids, no side longer than a fifth of the box's 26.9 mm diagonal: edges in
|
||||
// 4, 3 and 2 pieces, faces of 4 × 3, 4 × 2 and 3 × 2 cells, 2 · (12 + 8 + 6) · 2 triangles.
|
||||
assert_eq!(triangles, 104, "every triangle of the box");
|
||||
let vertex = block.bytes.glass.first().map_or(0, |r| r.start) as usize;
|
||||
let at = vertex * crate::gpu::TRI_SIZE;
|
||||
let colour = first_vertex(&second, block.bytes.vertices.get(at..).unwrap_or_default()).1;
|
||||
|
||||
Reference in New Issue
Block a user