mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 19:15:57 +00:00
A silhouette takes its facing from the surface's normals
A flat triangle beside the outline can tip the other way from the surface it approximates, and its edges joined the outline. On SMLib's sphere that made 192 segments instead of the equator's 64, 100 curves and 95 profiles from the tight fit of #7989. Each corner's normal is used where it is close to the triangle's own; a mesh without such normals (none, zero, not numbers, or averaged across creases) keeps the triangle's normal, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNXLXkZLQ6T7TSi4wswcmk
This commit is contained in:
@@ -1334,6 +1334,24 @@ fn section_of_polyline(plane: &Plane, pts: &[Vec3]) -> Vec<LinkGeom> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Which way the surface faces at triangle `t`: its normals at the corners, added. A flat triangle
|
||||
/// close to the outline can tip the other way from the surface it approximates (one whose far
|
||||
/// corner lies inside the chord of its near edge), which would add spurs to the outline. The
|
||||
/// triangle's own normal `flat` when a corner has none, or when they aren't that surface's: unit,
|
||||
/// and close to `flat` (normals averaged across a crease, as an imported mesh may have, are not).
|
||||
fn surface_facing(m: &Mesh, t: &[u32; 3], flat: Vec3) -> Vec3 {
|
||||
/// Least cosine between a corner's normal and the triangle's: about 26 degrees.
|
||||
const AGREES: f64 = 0.9;
|
||||
let mut sum = Vec3::ZERO;
|
||||
for i in t {
|
||||
match m.normals.get(*i as usize).and_then(|v| v.normalized()) {
|
||||
Some(v) if v.dot(flat) >= AGREES => sum = sum + v,
|
||||
_ => return flat,
|
||||
}
|
||||
}
|
||||
sum.normalized().unwrap_or(flat)
|
||||
}
|
||||
|
||||
/// The outline of a body seen along the sketch normal: mesh edges between triangles facing the
|
||||
/// sketch and triangles facing away (or edge-on), projected and fitted.
|
||||
fn silhouette(plane: &Plane, m: &Mesh) -> Vec<LinkGeom> {
|
||||
@@ -1346,8 +1364,8 @@ fn silhouette(plane: &Plane, m: &Mesh) -> Vec<LinkGeom> {
|
||||
for t in &m.triangles {
|
||||
let Some([a, b, c]) = m.tri(t) else { continue };
|
||||
let fnrm = (b - a).cross(c - a);
|
||||
let Some(un) = fnrm.normalized() else { continue };
|
||||
let front = un.dot(n) > 1e-6;
|
||||
let Some(flat) = fnrm.normalized() else { continue };
|
||||
let front = surface_facing(m, t, flat).dot(n) > 1e-6;
|
||||
for (p, r) in [(a, b), (b, c), (c, a)] {
|
||||
let (kp, kr) = (key(p), key(r));
|
||||
let k = if kp <= kr { (kp, kr) } else { (kr, kp) };
|
||||
|
||||
@@ -177,6 +177,26 @@ fn boundary_edges(mesh: &Mesh, mut edges: Vec<usize>, faces: &BTreeSet<usize>, p
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A cube's outline from above is its four sides whatever normals its mesh carries: none, too
|
||||
/// few, zero or not numbers fall back to each triangle's own normal, and so do normals
|
||||
/// averaged across its creases, as an imported mesh may have them.
|
||||
#[test]
|
||||
fn a_silhouette_takes_the_triangles_own_normal_where_the_mesh_has_no_surface_normal() {
|
||||
let corner = |i: u32| Vec3::new(f64::from(i & 1), f64::from((i >> 1) & 1), f64::from((i >> 2) & 1)) * 10.0;
|
||||
let positions: Vec<Vec3> = (0..8).map(corner).collect();
|
||||
let quads = [[0, 2, 3, 1], [4, 5, 7, 6], [0, 1, 5, 4], [2, 6, 7, 3], [0, 4, 6, 2], [1, 3, 7, 5]];
|
||||
let triangles: Vec<[u32; 3]> = quads.iter().flat_map(|[a, b, c, d]| [[*a, *b, *c], [*a, *c, *d]]).collect();
|
||||
let averaged: Vec<Vec3> = positions.iter().map(|p| (*p - Vec3::new(5.0, 5.0, 5.0)).normalized().unwrap_or(Vec3::Z)).collect();
|
||||
let nan = Vec3::new(f64::NAN, 0.0, 0.0);
|
||||
for normals in [Vec::new(), vec![Vec3::Z; 3], vec![Vec3::ZERO; 8], vec![nan; 8], vec![Vec3::new(f64::INFINITY, 0.0, 0.0); 8], averaged] {
|
||||
let tri_face = vec![0; triangles.len()];
|
||||
let mesh = Mesh { positions: positions.clone(), normals: normals.clone(), triangles: triangles.clone(), tri_face, ..Mesh::default() };
|
||||
let outline = super::super::silhouette(&Plane::XY, &mesh);
|
||||
let sides = outline.iter().filter(|g| matches!(g, solidcraft_sketch::LinkGeom::Line(a, b) if (a.dist(*b) - 10.0).abs() < 1e-9)).count();
|
||||
assert_eq!((outline.len(), sides), (4, 4), "{normals:?}: {outline:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_family_omits_only_exact_shared_topological_edges() {
|
||||
let mut mesh = Mesh::default();
|
||||
|
||||
@@ -1296,3 +1296,21 @@ fn a_thicker_angle_moves_the_projected_face_of_the_sketch_on_it() {
|
||||
check(&s);
|
||||
check(&Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
|
||||
}
|
||||
|
||||
/// A sphere's outline from above is its equator: one circle and one profile. Flat triangles beside
|
||||
/// the equator can tip the other way from the sphere, and their edges must not join the outline.
|
||||
#[test]
|
||||
fn a_spheres_silhouette_is_one_circle() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.sphere", json!({"radius": 8, "body_name": "Ball"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": -10, "name": "Below"}));
|
||||
let r = run(&mut s, "sketch.project", json!({"refs": [{"body": "Ball"}]}));
|
||||
let curves = r["links"][0]["curves"].as_array().unwrap();
|
||||
assert_eq!(curves.len(), 1, "{r}");
|
||||
let inspected = run(&mut s, "sketch.inspect", json!({}));
|
||||
let circle = inspected["curves"].as_array().unwrap().iter().find(|c| c["id"] == curves[0]).unwrap();
|
||||
assert_eq!(circle["type"], "circle", "{circle}");
|
||||
assert!((circle["radius"].as_f64().unwrap() - 8.0).abs() < 1e-6, "{circle}");
|
||||
let done = run(&mut s, "sketch.finish", json!({}));
|
||||
assert_eq!(done["profiles"], 1, "{done}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user