Loop blends: sharp corners between straight edges meet in mitres (ellipse arcs), arcs of exactly the blend's radius become sphere octants (pole-free), and loops plus the other edges blend in order (every edge of an extruded part: sphere and torus corners)
Oracle: 58/76. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
457ed7fed7
commit
7ca6e9278b
4 files changed
+223
-28
No files matched your search
+5
-3
@@ -9,7 +9,7 @@ features → parametric timeline) as an open, pure-Rust, agent-drivable applicat
|
||||
|---|---|
|
||||
| Where are we? | **M0 done; M1/M2 in progress.** Sketch with solver, extrude (taper, two-sided, through all), revolve, fillet/chamfer, holes, patterns, mirror, shell, draft, loft, sweep, split, construction planes, parameters that rebuild the timeline, STEP/STL export, desktop and browser (wasm) app with Fusion-style selection (hover/selected highlights, origin planes, click-to-pick sketch planes, multi-select, window/crossing box selection, animated view cube), CLI, MCP server, oracle harness. |
|
||||
| Command parity (in scope) | **139 / 541 (26%)** in-scope commands; SOLID + SKETCH **102 / 291 (35%)** — [docs/parity.md](docs/parity.md) |
|
||||
| Fusion oracle | **56 / 76 parts match** (batch 1: 29/29; batch 2 in progress) — [docs/oracle.md](docs/oracle.md) |
|
||||
| Fusion oracle | **58 / 76 parts match** (batch 1: 29/29; batch 2 in progress) — [docs/oracle.md](docs/oracle.md) |
|
||||
| Tests | 85 (solver, profiles, kernel booleans/blends/measures, expressions, timeline, file formats, camera, engine end-to-end, hostile-input fuzz over every command) |
|
||||
| Gates | `cargo xtask ci`: fmt, clippy -D warnings, tests, asset attribution, layering, wasm32 build — green |
|
||||
| Weighted parity estimate | **≈ 5%** of Fusion's Design workspace by importance (sketch + basic solids are the core, but surfaces, assemblies, sheet metal, CAM, drawings are untouched) |
|
||||
@@ -58,8 +58,10 @@ headline counts in-scope tabs only and lists the deferred ones separately.
|
||||
- Fillets and chamfers: straight edges (convex or concave) between planar faces with perpendicular
|
||||
planar end faces; whole smooth loops of a planar face, picked whole or by one edge (tangent
|
||||
chain), including circles and walls at any angle (boss bases and tops, bores, pocket floors,
|
||||
plate outlines); every edge of a convex planar body (sphere corners); fillets inside patterns.
|
||||
Edges between two curved faces and mixed corners are not supported yet.
|
||||
plate outlines), loops with sharp corners between straight edges (the blends meet in mitres),
|
||||
rounded corners rounded again (sphere octants, tori at concave corners); every edge of a
|
||||
convex planar body or of an extruded part; fillets inside patterns. Edges between two curved
|
||||
faces are not supported yet.
|
||||
- Coincident planar faces are pushed apart exactly when their neighbours stand perpendicular
|
||||
(extruded and box-like parts); other coincident configurations can still fail. Fully internal
|
||||
voids are not supported. Spheres are built from six pole-free patches so they
|
||||
|
||||
@@ -307,6 +307,17 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
|
||||
}
|
||||
}
|
||||
// Whole smooth loops of planar faces are blended in one go, loop by loop.
|
||||
// Loops and other edges together (every edge of an extruded part): the other edges
|
||||
// first, which leaves the loops smooth, then the loops.
|
||||
if let Some((groups, rest)) = loop_split(&cur, edges)?
|
||||
&& !rest.is_empty()
|
||||
&& rest.len() < edges.len()
|
||||
{
|
||||
let _ = groups;
|
||||
let loop_pts: Vec<Vec3> = edges.iter().filter(|p| !rest.iter().any(|q| q.dist(**p) < 1e-12)).copied().collect();
|
||||
cur = blend(&cur, &rest, r, shape, what)?;
|
||||
return blend(&cur, &loop_pts, r, shape, what);
|
||||
}
|
||||
if let Some(groups) = loop_groups(&cur, edges)? {
|
||||
for g in groups {
|
||||
let size = cur.size();
|
||||
@@ -382,6 +393,11 @@ fn edge_ids(cur: &Body, solid: &Solid, pts: &[Vec3]) -> Result<Vec<mt::EdgeID>>
|
||||
|
||||
/// When the edges make up whole boundary loops of planar faces: the edge points per loop.
|
||||
fn loop_groups(cur: &Body, pts: &[Vec3]) -> Result<Option<Vec<Vec<Vec3>>>> {
|
||||
Ok(loop_split(cur, pts)?.and_then(|(g, rest)| rest.is_empty().then_some(g)))
|
||||
}
|
||||
|
||||
/// Edges in whole loops of planar faces (points per loop), and the other edges' points.
|
||||
fn loop_split(cur: &Body, pts: &[Vec3]) -> Result<Option<(Vec<Vec<Vec3>>, Vec<Vec3>)>> {
|
||||
let solid = cur.deep_copy();
|
||||
let size = cur.size();
|
||||
let tol = (size * 2e-3).max(1e-3);
|
||||
@@ -430,8 +446,8 @@ fn loop_groups(cur: &Body, pts: &[Vec3]) -> Result<Option<Vec<Vec<Vec3>>>> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let all_in = ids.iter().all(|x| covered.contains(x));
|
||||
Ok((all_in && !groups.is_empty()).then_some(groups))
|
||||
let rest: Vec<Vec3> = per.iter().filter(|(_, id)| !covered.contains(id)).map(|(p, _)| *p).collect();
|
||||
Ok((!groups.is_empty()).then_some((groups, rest)))
|
||||
}
|
||||
|
||||
/// Constant-radius fillet of the edges nearest to the given points.
|
||||
|
||||
+157
-23
@@ -169,13 +169,19 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
let owner: Vec<usize> = split.iter().map(|x| x.0).collect();
|
||||
let segs: Vec<Seg> = split.iter().map(|x| x.1).collect();
|
||||
let k = segs.len();
|
||||
// Smooth joints.
|
||||
// Joints: smooth, or sharp between two lines (the blends then meet in a mitre).
|
||||
let mut mitre = vec![false; k];
|
||||
for i in 0..k {
|
||||
let (Some(prev), Some(cur)) = (segs.get((i + k - 1) % k), segs.get(i)) else { continue };
|
||||
let p = cur.start();
|
||||
let (Some(ta), Some(tb)) = (prev.tangent(p), cur.tangent(p)) else { return Err(unsupported("degenerate loop edge")) };
|
||||
if ta.dot(tb) < 1.0 - 1e-6 {
|
||||
return Err(unsupported("blending a loop with sharp corners"));
|
||||
if !(matches!(prev, Seg::Line { .. }) && matches!(cur, Seg::Line { .. })) || ta.dot(tb) < -1.0 + 1e-3 {
|
||||
return Err(unsupported("blending a loop with sharp corners next to arcs"));
|
||||
}
|
||||
if let Some(x) = mitre.get_mut(i) {
|
||||
*x = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Walls: the other face of each edge, perpendicular to the face; all on one side.
|
||||
@@ -226,26 +232,91 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
let sb = r / (phi / 2.0).tan();
|
||||
Some((m, t, sb, r / (phi / 2.0).sin()))
|
||||
};
|
||||
// The straight seam between the walls at a sharp joint (direction away from the face).
|
||||
let seam_dir = |i: usize, p: Vec3| -> Option<Vec3> {
|
||||
let o = owner.get(i)?;
|
||||
let v = edges.get(*o)?.front().clone();
|
||||
let loop_ids: Vec<mt::EdgeID> = edges.iter().map(|e| e.id()).collect();
|
||||
let mut dirs = Vec::new();
|
||||
for g in faces {
|
||||
for e in g.edge_iter() {
|
||||
if loop_ids.contains(&e.id()) || !(*e.front() == v || *e.back() == v) {
|
||||
continue;
|
||||
}
|
||||
let other = if *e.front() == v { e.back().clone() } else { e.front().clone() };
|
||||
if let Some(d) = (from_p3(other.point()) - p).normalized()
|
||||
&& !dirs.iter().any(|x: &Vec3| x.dot(d) > 1.0 - 1e-9)
|
||||
{
|
||||
dirs.push(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
match dirs[..] {
|
||||
[d] => Some(d),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let mut pa = Vec::with_capacity(k);
|
||||
let mut pb = Vec::with_capacity(k);
|
||||
let mut centres = Vec::with_capacity(k);
|
||||
for i in 0..k {
|
||||
let p = segs.get(i).map(|s| s.start()).unwrap_or_default();
|
||||
let (m, t, sb, dc) = frame(i, p).ok_or_else(|| unsupported("the face and wall meet at an angle the blend can't take"))?;
|
||||
pa.push(p + m * sb);
|
||||
pb.push(p + t * sb);
|
||||
centres.push(p + (m + t).normalized().unwrap_or(m) * dc);
|
||||
if !mitre.get(i).copied().unwrap_or(false) {
|
||||
pa.push(p + m * sb);
|
||||
pb.push(p + t * sb);
|
||||
continue;
|
||||
}
|
||||
// Sharp joint: the face rails meet where the two offset lines cross; the wall rails
|
||||
// meet on the seam.
|
||||
let ip = (i + k - 1) % k;
|
||||
let (m0, t0, sb0, _) = frame(ip, p).ok_or_else(|| unsupported("the face and wall meet at an angle the blend can't take"))?;
|
||||
let (Some(d0), Some(d1)) = (segs.get(ip).and_then(|x| x.tangent(p)), segs.get(i).and_then(|x| x.tangent(p))) else {
|
||||
return Err(unsupported("degenerate loop edge"));
|
||||
};
|
||||
let x = d0.cross(d1);
|
||||
let lam = ((m * sb - m0 * sb0).cross(d1)).dot(x) / x.len2();
|
||||
pa.push(p + m0 * sb0 + d0 * lam);
|
||||
let e = seam_dir(i, p).ok_or_else(|| unsupported("a sharp corner without one straight seam"))?;
|
||||
let (h0, h1) = (e.dot(t0), e.dot(t));
|
||||
if h0 < 1e-6 || h1 < 1e-6 || (sb0 / h0 - sb / h1).abs() > tol * 10.0 {
|
||||
return Err(unsupported("a sharp corner whose walls meet the blend differently"));
|
||||
}
|
||||
pb.push(p + e * (sb / h1));
|
||||
}
|
||||
// The offset arcs must not collapse.
|
||||
for s in &segs {
|
||||
// The offset arcs must not turn inside out; an arc of exactly the blend's radius shrinks
|
||||
// to a point (a rounded corner rounded again: the blend there is a sphere).
|
||||
let mut collapsed = vec![false; k];
|
||||
for (i, s) in segs.iter().enumerate() {
|
||||
if let Seg::Arc { a, c, .. } = *s {
|
||||
let toward = into(s, a).dot(c - a) > 0.0;
|
||||
if toward && a.dist(c) <= r + tol {
|
||||
if toward
|
||||
&& (a.dist(c) - r).abs() <= tol * 10.0
|
||||
&& !mitre.get(i).copied().unwrap_or(true)
|
||||
&& !mitre.get((i + 1) % k).copied().unwrap_or(true)
|
||||
{
|
||||
if let Some(x) = collapsed.get_mut(i) {
|
||||
*x = true;
|
||||
}
|
||||
} else if toward && a.dist(c) <= r + tol {
|
||||
return Err(unsupported("the blend is larger than a corner radius"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let va: Vec<mt::Vertex> = pa.iter().map(|p| builder::vertex(p3(*p))).collect();
|
||||
if collapsed.iter().all(|x| *x) {
|
||||
return Err(unsupported("the blend is larger than a corner radius"));
|
||||
}
|
||||
let mut va: Vec<mt::Vertex> = pa.iter().map(|p| builder::vertex(p3(*p))).collect();
|
||||
// A collapsed piece starts and ends at one vertex.
|
||||
for i in 0..k {
|
||||
if collapsed.get(i).copied().unwrap_or(false)
|
||||
&& let Some(v) = va.get(i).cloned()
|
||||
&& let Some(slot) = va.get_mut((i + 1) % k)
|
||||
{
|
||||
*slot = v;
|
||||
}
|
||||
}
|
||||
let vb: Vec<mt::Vertex> = pb.iter().map(|p| builder::vertex(p3(*p))).collect();
|
||||
let centre = |i: usize| centres.get(i).copied().unwrap_or_default();
|
||||
let profile_mid = |i: usize| {
|
||||
@@ -253,16 +324,50 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
c + (s.start() - c).normalized().unwrap_or(n) * r
|
||||
};
|
||||
// Profiles at the joints (face side → wall side).
|
||||
let profiles: Vec<mt::Edge> = (0..k)
|
||||
.map(|i| {
|
||||
let (Some(a), Some(b)) = (va.get(i), vb.get(i)) else {
|
||||
return builder::line(&builder::vertex(p3(Vec3::ZERO)), &builder::vertex(p3(Vec3::X)));
|
||||
};
|
||||
if round { builder::circle_arc(a, b, p3(profile_mid(i))) } else { builder::line(a, b) }
|
||||
})
|
||||
.collect();
|
||||
let mut profiles: Vec<mt::Edge> = Vec::with_capacity(k);
|
||||
for i in 0..k {
|
||||
let (Some(a), Some(b)) = (va.get(i), vb.get(i)) else { return Err(KernelError::Failed("joint".into())) };
|
||||
if !round {
|
||||
profiles.push(builder::line(a, b));
|
||||
continue;
|
||||
}
|
||||
if !mitre.get(i).copied().unwrap_or(false) {
|
||||
profiles.push(builder::circle_arc(a, b, p3(profile_mid(i))));
|
||||
continue;
|
||||
}
|
||||
// Mitre: the cross-section circle of this edge's blend where it passes the face-side
|
||||
// point, slid along the edge onto the mitre plane (an ellipse arc, on both blends).
|
||||
let p = segs.get(i).map(|x| x.start()).unwrap_or_default();
|
||||
let d1 = segs.get(i).and_then(|x| x.tangent(p)).ok_or_else(|| unsupported("tangent"))?;
|
||||
let (pa_i, pb_i) = (from_p3(a.point()), from_p3(b.point()));
|
||||
let lam = (pa_i - p).dot(d1);
|
||||
let (_, t, sb, _) = frame(i, p).ok_or_else(|| unsupported("frame"))?;
|
||||
let cc = centre(i) + d1 * lam;
|
||||
let q = p + t * sb + d1 * lam;
|
||||
let mid = cc + ((pa_i - cc) + (q - cc)).normalized().unwrap_or(t) * r;
|
||||
let arc = builder::circle_arc(&builder::vertex(p3(pa_i)), &builder::vertex(p3(q)), p3(mid));
|
||||
let nb = (pa_i - p).cross(pb_i - p).normalized().ok_or_else(|| unsupported("mitre plane"))?;
|
||||
let den = d1.dot(nb);
|
||||
if den.abs() < 1e-9 {
|
||||
return Err(unsupported("mitre plane along the edge"));
|
||||
}
|
||||
// x ↦ x − d1 ((x − p)·nb) / (d1·nb), column-major.
|
||||
let col = |j: usize| {
|
||||
let ej = [Vec3::X, Vec3::Y, Vec3::Z][j];
|
||||
ej - d1 * (ej.dot(nb) / den)
|
||||
};
|
||||
let (c0, c1, c2) = (col(0), col(1), col(2));
|
||||
let tr = d1 * (p.dot(nb) / den);
|
||||
let mat = mt::Matrix4::new(c0.x, c0.y, c0.z, 0.0, c1.x, c1.y, c1.z, 0.0, c2.x, c2.y, c2.z, 0.0, tr.x, tr.y, tr.z, 1.0);
|
||||
let mut curve = arc.curve();
|
||||
mt::Transformed::transform_by(&mut curve, mat);
|
||||
profiles.push(mt::Edge::new(a, b, curve));
|
||||
}
|
||||
let rail = |vs: &[mt::Vertex], i: usize, offset: &dyn Fn(Vec3, &Seg) -> Vec3| -> Option<mt::Edge> {
|
||||
let s = segs.get(i)?;
|
||||
if std::ptr::eq(vs, va.as_slice()) && collapsed.get(i).copied().unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
let (a, b) = (vs.get(i)?, vs.get((i + 1) % k)?);
|
||||
Some(match *s {
|
||||
Seg::Line { .. } => builder::line(a, b),
|
||||
@@ -278,8 +383,10 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
let i = piece_of(s);
|
||||
frame(i, p).map(|(_, t, sb, _)| p + t * sb).unwrap_or(p)
|
||||
};
|
||||
let face_rails: Vec<mt::Edge> =
|
||||
(0..k).map(|i| rail(&va, i, &face_off)).collect::<Option<_>>().ok_or_else(|| KernelError::Failed("rail".into()))?;
|
||||
let face_rails: Vec<Option<mt::Edge>> = (0..k).map(|i| rail(&va, i, &face_off)).collect();
|
||||
if face_rails.iter().zip(&collapsed).any(|(r, c)| r.is_none() && !c) {
|
||||
return Err(KernelError::Failed("rail".into()));
|
||||
}
|
||||
let wall_rails: Vec<mt::Edge> =
|
||||
(0..k).map(|i| rail(&vb, i, &wall_off)).collect::<Option<_>>().ok_or_else(|| KernelError::Failed("rail".into()))?;
|
||||
// Seams between neighbouring walls at the joints get shorter.
|
||||
@@ -314,14 +421,14 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
}
|
||||
// Rebuild faces.
|
||||
let idx_of = |id: mt::EdgeID| loop_ids.iter().position(|x| *x == id);
|
||||
let rebuild = |g: &mt::Face, rails: &[mt::Edge]| -> Result<mt::Face> {
|
||||
let rebuild = |g: &mt::Face, rails: &[Option<mt::Edge>]| -> Result<mt::Face> {
|
||||
let mut wires = Vec::new();
|
||||
for w in g.absolute_boundaries() {
|
||||
let mut es = Vec::new();
|
||||
for e in w.edge_iter() {
|
||||
if let Some(i) = idx_of(e.id()) {
|
||||
// Rails run in loop order; an edge used the other way takes them reversed.
|
||||
let mine: Vec<&mt::Edge> = owner.iter().zip(rails).filter(|(o, _)| **o == i).map(|(_, r)| r).collect();
|
||||
let mine: Vec<&mt::Edge> = owner.iter().zip(rails).filter(|(o, _)| **o == i).filter_map(|(_, r)| r.as_ref()).collect();
|
||||
let same = edges.get(i).is_some_and(|le| le.orientation() == e.orientation());
|
||||
if same {
|
||||
es.extend(mine.into_iter().cloned());
|
||||
@@ -346,7 +453,8 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
if gi == fi {
|
||||
out.push(rebuild(g, &face_rails)?);
|
||||
} else if walls.contains(&gi) {
|
||||
out.push(rebuild(g, &wall_rails)?);
|
||||
let wr: Vec<Option<mt::Edge>> = wall_rails.iter().cloned().map(Some).collect();
|
||||
out.push(rebuild(g, &wr)?);
|
||||
} else if g.edge_iter().any(|e| seam_subst.contains_key(&e.id())) {
|
||||
out.push(rebuild(g, &[])?);
|
||||
} else {
|
||||
@@ -362,7 +470,30 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
continue;
|
||||
};
|
||||
let swept: mt::Shell = match *s {
|
||||
Seg::Line { a, b } => vec![builder::tsweep(pr, v3(b - a))].into(),
|
||||
Seg::Line { a, b } => {
|
||||
// The cross-section at the start, swept along the whole edge and beyond (the
|
||||
// face may reach past the edge's ends at a mitre).
|
||||
let d = (b - a).normalized().ok_or_else(|| unsupported("degenerate loop edge"))?;
|
||||
let (_, t, sb, _) = frame(i, a).ok_or_else(|| unsupported("frame"))?;
|
||||
let m = into(s, a);
|
||||
let ext = size;
|
||||
let back = d * -ext;
|
||||
let (fa, wa) = (builder::vertex(p3(a + m * sb + back)), builder::vertex(p3(a + t * sb + back)));
|
||||
let base = if round { builder::circle_arc(&fa, &wa, p3(profile_mid(i) + back)) } else { builder::line(&fa, &wa) };
|
||||
vec![builder::tsweep(&base, v3(b - a + d * (2.0 * ext)))].into()
|
||||
}
|
||||
Seg::Arc { .. } if collapsed.get(i).copied().unwrap_or(false) => {
|
||||
// A sphere about the profile's centre, its poles well away from the corner
|
||||
// patch (a pole inside a face's corner confuses the meshing).
|
||||
let c = centre(i);
|
||||
let corners = [from_p3(pr.front().point()), from_p3(pr.back().point()), from_p3(pr_next.back().point())];
|
||||
let g = corners.iter().fold(Vec3::ZERO, |acc, q| acc + (*q - c).normalized().unwrap_or(Vec3::ZERO));
|
||||
let g = g.normalized().ok_or_else(|| unsupported("sphere corner"))?;
|
||||
let w = g.any_perp();
|
||||
let (top, eq, bottom) = (builder::vertex(p3(c + w * r)), p3(c + g * r), builder::vertex(p3(c - w * r)));
|
||||
let meridian = builder::circle_arc(&top, &bottom, eq);
|
||||
builder::rsweep(&meridian, p3(c), v3(w), mt::Rad(std::f64::consts::TAU))
|
||||
}
|
||||
Seg::Arc { c, axis, angle, .. } => builder::rsweep(pr, p3(c), v3(axis), mt::Rad(angle)),
|
||||
};
|
||||
let mut surf = swept.face_iter().next().map(|x| x.oriented_surface()).ok_or_else(|| KernelError::Failed("blend surface".into()))?;
|
||||
@@ -375,7 +506,10 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
surf = mt::Invertible::inverse(&surf);
|
||||
}
|
||||
}
|
||||
let w: mt::Wire = vec![fr.inverse(), pr.clone(), wr.clone(), pr_next.inverse()].into();
|
||||
let w: mt::Wire = match fr {
|
||||
Some(fr) => vec![fr.inverse(), pr.clone(), wr.clone(), pr_next.inverse()].into(),
|
||||
None => vec![pr.clone(), wr.clone(), pr_next.inverse()].into(),
|
||||
};
|
||||
let face = mt::Face::try_new(vec![w.clone()], surf.clone()).map_err(|e| KernelError::Failed(format!("blend face: {e}")))?;
|
||||
out.push(face);
|
||||
}
|
||||
|
||||
@@ -958,3 +958,46 @@ fn draft_of_a_round_ended_pocket() {
|
||||
let v = measure(&d).unwrap().volume;
|
||||
assert!(rel(v, 80.0 * 60.0 * 25.0 - pocket) < 1e-4, "{v} vs {}", 80.0 * 60.0 * 25.0 - pocket);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded_corners_rounded_again_make_spheres() {
|
||||
// Vertical edges first, then the top and bottom loops with the same radius: the corners
|
||||
// become sphere octants, as if every edge were rounded at once.
|
||||
let (a, b, c, r) = (40.0, 30.0, 20.0, 2.0);
|
||||
let bx = box_solid(Vec3::ZERO, Vec3::new(a, b, c)).unwrap();
|
||||
let vert: Vec<Vec3> = [(0.0, 0.0), (a, 0.0), (a, b), (0.0, b)].iter().map(|(x, y)| Vec3::new(*x, *y, c / 2.0)).collect();
|
||||
let f = fillet(&bx, &vert, r).unwrap();
|
||||
let f = fillet(&f, &[Vec3::new(a / 2.0, 0.0, c)], r).unwrap();
|
||||
let f = fillet(&f, &[Vec3::new(a / 2.0, 0.0, 0.0)], r).unwrap();
|
||||
let (ai, bi, ci) = (a - 2.0 * r, b - 2.0 * r, c - 2.0 * r);
|
||||
let area = 2.0 * (ai * bi + bi * ci + ai * ci) + PI * r * (ai + bi + ci) * 2.0 + 4.0 * PI * r * r;
|
||||
let vol = ai * bi * ci + 2.0 * r * (ai * bi + bi * ci + ai * ci) + PI * r * r * (ai + bi + ci) + 4.0 / 3.0 * PI * r * r * r;
|
||||
let m = measure(&f).unwrap();
|
||||
println!("area {} want {area}; volume {} want {vol}; faces {:?}", m.area, m.volume, m.merged);
|
||||
assert!(rel(m.volume, vol) < 1e-4, "volume {} vs {vol}", m.volume);
|
||||
assert!(rel(m.area, area) < 1e-4, "area {} vs {area}", m.area);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_edge_of_an_l_block() {
|
||||
// An L (60 × 40, legs 8 wide) 20 thick with all 18 edges rounded by 2: the inner vertical
|
||||
// edge is concave (its corners become tori), the others convex (sphere corners).
|
||||
let pts = [(0.0, 0.0), (60.0, 0.0), (60.0, 8.0), (8.0, 8.0), (8.0, 40.0), (0.0, 40.0)];
|
||||
let outer = Loop2::polygon(&pts.iter().map(|(x, y)| Vec2::new(*x, *y)).collect::<Vec<_>>());
|
||||
let l = extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0.0, 20.0).unwrap().pop().unwrap();
|
||||
let edges: Vec<Vec3> = l.edges(0.1).unwrap().iter().map(|e| e.mid).collect();
|
||||
let f = fillet(&l, &edges, 2.0).unwrap();
|
||||
let m = measure(&f).unwrap();
|
||||
// Area by pieces: walls (lengths less the roundings, 16 high), top and bottom (the L shrunk
|
||||
// by 2 with a radius-4 inner corner), quarter cylinders along every straight edge, ten
|
||||
// sphere octants, and two torus quarters (Pappus: arc π, centroid radius 4 − 4/π, angle π/2).
|
||||
let walls = 16.0 * (56.0 + 4.0 + 48.0 + 28.0 + 4.0 + 36.0);
|
||||
let caps = 2.0 * (56.0 * 4.0 + 4.0 * 32.0 + 16.0 - 4.0 * PI);
|
||||
let vertical = 6.0 * PI * 16.0;
|
||||
let horizontal = 2.0 * PI * (56.0 + 4.0 + 48.0 + 28.0 + 4.0 + 36.0);
|
||||
let spheres = 10.0 * 2.0 * PI;
|
||||
let tori = 2.0 * PI * (4.0 - 4.0 / PI) * PI / 2.0;
|
||||
let area = walls + caps + vertical + horizontal + spheres + tori;
|
||||
assert!(rel(m.area, area) < 1e-4, "area {} vs {area}", m.area);
|
||||
assert_eq!((m.merged.faces, m.merged.edges, m.merged.vertices), (38, 74, 38));
|
||||
}
|
||||
Reference in new issue
Block a user