mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5774d27c2f |
@@ -1185,11 +1185,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// The 10 × 20 × 30 box meshes as two triangles a face (a flat face needs no interior
|
||||
/// nodes), 12 triangles on its 8 corners (unwelded, 36), and one normal per face.
|
||||
#[test]
|
||||
fn obj_export_is_welded_and_keeps_names() {
|
||||
let (_, m) = model();
|
||||
@@ -1200,10 +1197,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 ")), (64, 6, 124), "{o}");
|
||||
assert_eq!((count("v "), count("vn "), count("f ")), (8, 6, 12), "{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..=64).contains(i)));
|
||||
assert!(ids.iter().all(|i| (1..=8).contains(i)));
|
||||
}
|
||||
|
||||
/// The OBJ export carries the colours of the bodies: the mesh file names the material
|
||||
|
||||
@@ -179,7 +179,12 @@ pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result
|
||||
.into_iter()
|
||||
.collect::<Result<_>>()?;
|
||||
let face_h = |f: usize| sizes.get(f).map_or(rules.max_len, |(h, _)| *h);
|
||||
let edge_h: Vec<f64> = edge_faces.iter().map(|fs| fs.iter().map(|&f| face_h(f)).fold(rules.max_len, f64::min)).collect();
|
||||
// A flat face asks for no size (infinite): an edge between flat faces is whole, one beside
|
||||
// a curved face takes the curved face's size, so both meet at the same nodes.
|
||||
let edge_h: Vec<f64> = edge_faces
|
||||
.iter()
|
||||
.map(|fs| if fs.is_empty() { rules.max_len } else { fs.iter().map(|&f| face_h(f)).fold(f64::INFINITY, f64::min) })
|
||||
.collect();
|
||||
let jobs: Vec<(usize, f64)> = edge_h.into_iter().enumerate().collect();
|
||||
let solved = b.edges.iter().filter(|e| matches!(e.curve, Curve::SurfaceIntersection { .. })).count();
|
||||
let mut nodes: Vec<edges::Nodes> = crate::par::map("edge nodes", &jobs, solved >= PARALLEL_CURVES || jobs.len() >= PARALLEL_EDGES, |(e, h)| {
|
||||
@@ -1027,15 +1032,18 @@ fn plan_face(
|
||||
// Constrained Delaunay in (u, v) scaled to millimetres: equilateral seeds of side `h`,
|
||||
// refined where triangles leave the surface, are too long or bend too much, then shaped by
|
||||
// Ruppert refinement.
|
||||
let planar = matches!(s, EvalSurface::Plane { .. });
|
||||
let bare = planar && h.is_infinite();
|
||||
let polys: Vec<&[[f64; 2]]> = ranges.iter().filter_map(|r| uvs.get(r.clone())).collect();
|
||||
let step = [h / speed[0], h / speed[1]];
|
||||
let delaunay = || {
|
||||
let seeds = cdt::lattice(bbox, step, &polys, speed);
|
||||
// A flat face with no size asked of it: the boundary nodes alone, no interior seeds.
|
||||
let seeds = if bare { Vec::new() } else { cdt::lattice(bbox, step, &polys, speed) };
|
||||
let eval = |u: f64, v: f64| s.derivs(u, v).map(|(p, su, sv)| (p, su.cross(sv).normalized()));
|
||||
let sag = cdt::Sag { eval: &eval, tol, h, turn: rules.turn, scale: speed };
|
||||
let fill = cdt::Fill {
|
||||
seeds: &seeds,
|
||||
refine: Some((sag, MAX_REFINE)),
|
||||
refine: if bare { None } else { Some((sag, MAX_REFINE)) },
|
||||
shape: !matches!(s, EvalSurface::Plane { .. }),
|
||||
lenient: lenient == Lenient::Yes,
|
||||
};
|
||||
@@ -1043,7 +1051,6 @@ fn plan_face(
|
||||
};
|
||||
// Planar and curved faces alike: a structured grid where the boundary runs along the
|
||||
// parameter box (a rectangle, a cylinder half), a Coons grid on four corners, else Delaunay.
|
||||
let planar = matches!(s, EvalSurface::Plane { .. });
|
||||
// The lenient fill is the constrained Delaunay alone: the caller names the face in the stats.
|
||||
let (extra, triangles) = if lenient == Lenient::Yes {
|
||||
delaunay()?
|
||||
@@ -1051,7 +1058,7 @@ fn plan_face(
|
||||
let mut us = uv::divide(s, 0, bbox[0], bbox[1], rules, h, MAX_LINES);
|
||||
let mut vs = uv::divide(s, 1, bbox[1], bbox[0], rules, h, MAX_LINES);
|
||||
grid::thin(&mut us, &mut vs, grid::MAX_FACE_GRID);
|
||||
let structured = match (ranges.len(), if seamed { None } else { grid::quad(&uvs) }) {
|
||||
let structured = match (ranges.len(), if seamed || bare { None } else { grid::quad(&uvs) }) {
|
||||
(1, Some(q)) => {
|
||||
// Interior lines: the boundary's own where opposite sides agree (refined where the
|
||||
// surface turns more between them inside the face), else the division.
|
||||
@@ -1076,7 +1083,8 @@ fn plan_face(
|
||||
_ => None,
|
||||
};
|
||||
let structured = match structured {
|
||||
None if ranges.len() == 1 && !seamed => {
|
||||
// A flat face with no size asked of it is its boundary alone (the Delaunay below).
|
||||
None if ranges.len() == 1 && !seamed && !bare => {
|
||||
let fine = |a: [f64; 2], b: [f64; 2]| uv::fine_between(s, rules, h, a, b);
|
||||
coons::coons(&uvs, &fixed, speed, grid::MAX_FACE_GRID, h, Some(&fine))
|
||||
}
|
||||
@@ -1108,7 +1116,7 @@ fn plan_face(
|
||||
let trimmed = if seamed || strip {
|
||||
None
|
||||
} else if planar {
|
||||
if ranges.len() == 1 { plan_pieces(&face) } else { None }
|
||||
if ranges.len() == 1 && h.is_finite() { plan_pieces(&face) } else { None }
|
||||
} else {
|
||||
plan_pieces(&face).or_else(|| {
|
||||
let traces = motorcycle::traces(&uvs, &ranges, speed, 0.25 * h);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Target triangle size: one side length per face, so triangles come out about equilateral
|
||||
//! (well-shaped triangles shade evenly) and as small as the surface needs.
|
||||
//!
|
||||
//! A face's size is the smallest of the body's longest side, the length over which the surface
|
||||
//! A flat face (a plane, or a spline lying in one) has no size: any triangulation of its
|
||||
//! boundary is exact, so it gets no interior cells and its straight edges no extra nodes.
|
||||
//! A curved face's size is the smallest of the body's longest side, the length over which the surface
|
||||
//! normal turns the caller's facet angle (when one is given, see [`Turn`]), and the chord that
|
||||
//! sags the tolerance; an edge takes the smaller size of its two faces, so a curved face and
|
||||
//! its flat neighbour meet at the same spacing.
|
||||
@@ -107,11 +109,12 @@ pub(crate) fn extent(points: impl Iterator<Item = Vec3>) -> f64 {
|
||||
/// so the size is never larger than the face needs).
|
||||
pub(crate) fn face_size(s: &EvalSurface, dom: &Domain, rules: &Rules) -> (f64, [f64; 2]) {
|
||||
if let EvalSurface::Plane { du, dv, .. } = s {
|
||||
return (rules.max_len, [du.len(), dv.len()]);
|
||||
return (f64::INFINITY, [du.len(), dv.len()]);
|
||||
}
|
||||
let (Some(ru), Some(rv)) = (dom.range[0], dom.range[1]) else { return (rules.max_len, [1.0, 1.0]) };
|
||||
let flat = planar(s);
|
||||
// A flat spline (sheet-metal walls, chamfers): no curvature to look for, only the speeds.
|
||||
let samples = if planar(s) { 3 } else { SAMPLES };
|
||||
let samples = if flat { 3 } else { SAMPLES };
|
||||
let normal = |u: f64, v: f64| -> Option<(Vec3, Vec3, [f64; 2])> {
|
||||
let (p, su, sv) = s.derivs(u, v)?;
|
||||
Some((p, su.cross(sv).normalized()?, [su.len(), sv.len()]))
|
||||
@@ -141,7 +144,9 @@ pub(crate) fn face_size(s: &EvalSurface, dom: &Domain, rules: &Rules) -> (f64, [
|
||||
}
|
||||
}
|
||||
let speed = if count > 0 { [speed[0] / count as f64, speed[1] / count as f64] } else { [1.0, 1.0] };
|
||||
(rules.for_curvature(k_max), speed.map(|x| if x.is_finite() && x > 0.0 { x } else { 1.0 }))
|
||||
// A flat face is exact for any triangulation of its boundary: no length cap.
|
||||
let size = if flat { f64::INFINITY } else { rules.for_curvature(k_max) };
|
||||
(size, speed.map(|x| if x.is_finite() && x > 0.0 { x } else { 1.0 }))
|
||||
}
|
||||
|
||||
/// A spline surface whose control points lie in one plane, with positive weights: the
|
||||
@@ -211,7 +216,7 @@ mod tests {
|
||||
assert!(!planar(&patch(0.5)));
|
||||
let rules = Rules::new(0.01, 100.0, THIRTY);
|
||||
let (h, speed) = face_size(&patch(0.0), &super::super::uv::domain(&patch(0.0)), &rules);
|
||||
assert_eq!(h, rules.max_len);
|
||||
assert_eq!(h, f64::INFINITY);
|
||||
assert!((speed[0] - 4.0).abs() < 1e-9 && (speed[1] - 3.0).abs() < 1e-9, "{speed:?}");
|
||||
// The twisted patch (a hyperbolic paraboloid) has curvature: a smaller side.
|
||||
assert!(face_size(&patch(0.5), &super::super::uv::domain(&patch(0.5)), &rules).0 < rules.max_len);
|
||||
@@ -232,9 +237,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plane_takes_the_longest_side() {
|
||||
fn a_plane_has_no_size_cap() {
|
||||
let s = EvalSurface::Plane { origin: Vec3::ZERO, du: Vec3::X, dv: Vec3::Y };
|
||||
let rules = Rules::new(0.01, 100.0, THIRTY);
|
||||
assert_eq!(face_size(&s, &super::super::uv::domain(&s), &rules).0, 20.0);
|
||||
assert_eq!(face_size(&s, &super::super::uv::domain(&s), &rules).0, f64::INFINITY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,31 +69,45 @@ fn cut(a: &Body, b: &Body) -> Body {
|
||||
|
||||
const PI: f64 = std::f64::consts::PI;
|
||||
|
||||
/// Smallest corner angle (degrees) over the mesh.
|
||||
fn min_angle(m: &Mesh) -> f64 {
|
||||
let mut least = 180.0f64;
|
||||
for t in &m.triangles {
|
||||
let [a, b, c] = t.map(|i| m.positions[i as usize]);
|
||||
for (p, q, r) in [(a, b, c), (b, c, a), (c, a, b)] {
|
||||
let (u, v) = (q - p, r - p);
|
||||
if let (Some(u), Some(v)) = (u.normalized(), v.normalized()) {
|
||||
least = least.min(u.dot(v).clamp(-1.0, 1.0).acos().to_degrees());
|
||||
}
|
||||
}
|
||||
/// A flat face is exact for any triangulation of its boundary, so a box is two triangles a
|
||||
/// face whatever its size (it was a grid capped at a fifth of the body's diagonal: 124
|
||||
/// triangles for 20 x 10 x 5).
|
||||
#[test]
|
||||
fn a_box_is_two_triangles_a_face() {
|
||||
for (name, size) in [("20 x 10 x 5", Vec3::new(20.0, 10.0, 5.0)), ("40 x 30 x 20", Vec3::new(40.0, 30.0, 20.0))] {
|
||||
let b = ok(crate::box_solid(Vec3::ZERO, size));
|
||||
let (m, s) = plan_body(&b, 0.05);
|
||||
assert_eq!(s.planar, 6);
|
||||
assert_watertight(&m, name);
|
||||
assert_eq!(m.triangles.len(), 12, "{name}: {:?}", (0..6).map(|i| m.tri_face.iter().filter(|t| **t == i).count()).collect::<Vec<_>>());
|
||||
assert!(max_chord(&b, &m) <= 1e-9, "{name}: flat faces are exact");
|
||||
}
|
||||
least
|
||||
}
|
||||
|
||||
/// Flat faces beside curved ones stay joined: the straight edge between a plane and a fillet
|
||||
/// takes the fillet's spacing, and the plane's boundary uses those nodes.
|
||||
#[test]
|
||||
fn a_box_is_a_regular_grid_on_every_face() {
|
||||
// 40 × 30 × 20: sides at most 1/5 of the diagonal (10.77 mm), so 4 × 3 × 2 cells.
|
||||
let b = ok(crate::box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)));
|
||||
let (m, s) = plan_body(&b, 0.05);
|
||||
assert_eq!(s.planar, 6);
|
||||
assert_watertight(&m, "box");
|
||||
let cells = 2 * (4 * 3 + 4 * 2 + 3 * 2);
|
||||
assert_eq!(m.triangles.len(), 2 * cells, "two triangles per grid cell");
|
||||
assert!(min_angle(&m) > 40.0, "{}", min_angle(&m));
|
||||
fn flat_faces_beside_curved_ones_stay_watertight_and_within_the_chord() {
|
||||
let tol = 0.01;
|
||||
let bx = ok(crate::box_solid(Vec3::ZERO, Vec3::new(20.0, 10.0, 5.0)));
|
||||
let rounded = ok(crate::fillet(&bx, &[Vec3::new(10.0, 0.0, 5.0), Vec3::new(20.0, 5.0, 5.0), Vec3::new(10.0, 10.0, 5.0), Vec3::new(0.0, 5.0, 5.0)], 1.5));
|
||||
let pin = ok(crate::cylinder(Vec3::new(10.0, 5.0, -1.0), Vec3::Z, 2.0, 7.0));
|
||||
let holed = cut(&bx, &pin);
|
||||
for (name, body) in [("filleted box", &rounded), ("box with a hole", &holed)] {
|
||||
let m = closed_display_mesh(name, body, tol);
|
||||
let chord = max_chord(body, &m);
|
||||
assert!(chord <= tol * 1.05, "{name}: chord {chord}");
|
||||
// The flat faces carry no interior nodes: far fewer triangles than a grid at the
|
||||
// curved neighbours' spacing would give.
|
||||
let (flat, curved) = (0..body.face_count()).fold((0, 0), |(f, c), i| {
|
||||
let n = m.tri_face.iter().filter(|t| **t as usize == i).count();
|
||||
match crate::face_surface(body, i) {
|
||||
Some(crate::FaceSurface::Plane { .. }) => (f + n, c),
|
||||
_ => (f, c + n),
|
||||
}
|
||||
});
|
||||
assert!(flat < curved, "{name}: {flat} flat vs {curved} curved triangles");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user